[ADD] account_spread_cost_revenue
This commit is contained in:
parent
75fc69fc2c
commit
f0e84cebd9
170
account_spread_cost_revenue/README.rst
Normal file
170
account_spread_cost_revenue/README.rst
Normal file
@ -0,0 +1,170 @@
|
||||
===================
|
||||
Cost-Revenue Spread
|
||||
===================
|
||||
|
||||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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/11.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-11-0/account-financial-tools-11-0-account_spread_cost_revenue
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
|
||||
:target: https://runbot.odoo-community.org/runbot/92/11.0
|
||||
:alt: Try me on Runbot
|
||||
|
||||
|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*.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Under Invoicing -> Adviser -> Accounting Entries -> 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:: static/description/spread.png
|
||||
:alt: Create a new spread board
|
||||
|
||||
Click on the button on the top-left to calculate the spread lines.
|
||||
|
||||
.. figure:: 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:: 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 unposted, so you should post them manually one by one.
|
||||
To allow the automatic posting of the accounting moves, set the flag *Auto-post lines* to True.
|
||||
|
||||
Create an invoice or vendor bill in draft. On its lines, the spreading right-arrow icon are displayed in dark-grey color.
|
||||
|
||||
.. figure:: 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:: 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 *Reconciled entries*: the moves of the spread lines are reconciled 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.
|
||||
|
||||
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.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* Verify last day of month
|
||||
* Not yet compatible with cutoff module: create module for adaptation?
|
||||
|
||||
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 smashing it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_spread_cost_revenue%0Aversion:%2011.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>
|
||||
|
||||
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.
|
||||
|
||||
.. |maintainer-astirpe| image:: https://github.com/astirpe.png?size=40px
|
||||
:target: https://github.com/astirpe
|
||||
:alt: astirpe
|
||||
|
||||
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|
||||
|
||||
|maintainer-astirpe|
|
||||
|
||||
This module is part of the `OCA/account-financial-tools <https://github.com/OCA/account-financial-tools/tree/11.0/account_spread_cost_revenue>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
4
account_spread_cost_revenue/__init__.py
Normal file
4
account_spread_cost_revenue/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
29
account_spread_cost_revenue/__manifest__.py
Normal file
29
account_spread_cost_revenue/__manifest__.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright 2016-2018 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": "11.0.1.0.0",
|
||||
"development_status": "Beta",
|
||||
"author": "Onestein,Odoo Community Association (OCA)",
|
||||
"maintainers": ["astirpe"],
|
||||
"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_invoice.xml",
|
||||
"views/res_company.xml",
|
||||
"views/account_spread_template.xml",
|
||||
"templates/assets.xml",
|
||||
"wizards/account_spread_invoice_line_link_wizard.xml",
|
||||
"data/spread_cron.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
17
account_spread_cost_revenue/data/spread_cron.xml
Normal file
17
account_spread_cost_revenue/data/spread_cron.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?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>
|
8
account_spread_cost_revenue/models/__init__.py
Normal file
8
account_spread_cost_revenue/models/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account_invoice
|
||||
from . import account_invoice_line
|
||||
from . import account_spread_line
|
||||
from . import account_spread
|
||||
from . import account_spread_template
|
||||
from . import res_company
|
45
account_spread_cost_revenue/models/account_invoice.py
Normal file
45
account_spread_cost_revenue/models/account_invoice.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Copyright 2016-2018 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class AccountInvoice(models.Model):
|
||||
_inherit = 'account.invoice'
|
||||
|
||||
@api.multi
|
||||
def action_move_create(self):
|
||||
"""Invoked when validating the invoices."""
|
||||
res = super().action_move_create()
|
||||
for invoice in self:
|
||||
spreads = invoice.invoice_line_ids.mapped('spread_id')
|
||||
spreads.compute_spread_board()
|
||||
spreads.reconcile_spread_moves()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def invoice_line_move_line_get(self):
|
||||
"""Copying expense/revenue account from spread to move lines."""
|
||||
res = super().invoice_line_move_line_get()
|
||||
for line in res:
|
||||
invl_id = line.get('invl_id')
|
||||
invl = self.env['account.invoice.line'].browse(invl_id)
|
||||
if invl.spread_id:
|
||||
if invl.invoice_id.type in ('out_invoice', 'in_refund'):
|
||||
account = invl.spread_id.debit_account_id
|
||||
else:
|
||||
account = invl.spread_id.credit_account_id
|
||||
line['account_id'] = account.id
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
"""Cancel the spread lines and their related moves when
|
||||
the invoice is canceled."""
|
||||
res = super().action_cancel()
|
||||
for invoice_line in self.mapped('invoice_line_ids'):
|
||||
moves = invoice_line.spread_id.line_ids.mapped('move_id')
|
||||
moves.button_cancel()
|
||||
moves.unlink()
|
||||
invoice_line.spread_id.line_ids.unlink()
|
||||
return res
|
65
account_spread_cost_revenue/models/account_invoice_line.py
Normal file
65
account_spread_cost_revenue/models/account_invoice_line.py
Normal file
@ -0,0 +1,65 @@
|
||||
# Copyright 2016-2018 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class AccountInvoiceLine(models.Model):
|
||||
_inherit = 'account.invoice.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', 'invoice_id.state')
|
||||
def _compute_spread_check(self):
|
||||
for line in self:
|
||||
if line.spread_id:
|
||||
line.spread_check = 'linked'
|
||||
elif line.invoice_id.state == 'draft':
|
||||
line.spread_check = 'unlinked'
|
||||
else:
|
||||
line.spread_check = 'unavailable'
|
||||
|
||||
@api.multi
|
||||
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_type': 'form',
|
||||
'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.invoice_id.company_id.id,
|
||||
)
|
||||
return {
|
||||
'name': _('Link Invoice Line with Spread Board'),
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'account.spread.invoice.line.link.wizard',
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
461
account_spread_cost_revenue/models/account_spread.py
Normal file
461
account_spread_cost_revenue/models/account_spread.py
Normal file
@ -0,0 +1,461 @@
|
||||
# Copyright 2018 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.addons import decimal_precision as dp
|
||||
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']
|
||||
|
||||
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)
|
||||
credit_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='Credit Account',
|
||||
required=True)
|
||||
debit_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='Debit Account',
|
||||
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=dp.get_precision('Account'),
|
||||
compute='_compute_amounts')
|
||||
unposted_amount = fields.Float(
|
||||
digits=dp.get_precision('Account'),
|
||||
compute='_compute_amounts')
|
||||
posted_amount = fields.Float(
|
||||
digits=dp.get_precision('Account'),
|
||||
compute='_compute_amounts')
|
||||
total_amount = fields.Float(
|
||||
digits=dp.get_precision('Account'),
|
||||
compute='_compute_amounts')
|
||||
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',
|
||||
string='Journal',
|
||||
required=True)
|
||||
invoice_line_ids = fields.One2many(
|
||||
'account.invoice.line',
|
||||
'spread_id',
|
||||
copy=False,
|
||||
string='Invoice Lines')
|
||||
invoice_line_id = fields.Many2one(
|
||||
'account.invoice.line',
|
||||
string='Invoice line',
|
||||
compute='_compute_invoice_line',
|
||||
inverse='_inverse_invoice_line',
|
||||
store=True)
|
||||
invoice_id = fields.Many2one(
|
||||
related='invoice_line_id.invoice_id',
|
||||
readonly=True,
|
||||
store=True,
|
||||
string='Invoice')
|
||||
estimated_amount = fields.Float(digits=dp.get_precision('Account'))
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
default=lambda self: self.env.user.company_id,
|
||||
string='Company',
|
||||
required=True)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
string='Currency',
|
||||
required=True,
|
||||
default=lambda self: self.env.user.company_id.currency_id.id)
|
||||
account_analytic_id = fields.Many2one(
|
||||
'account.analytic.account',
|
||||
string='Analytic Account')
|
||||
analytic_tag_ids = fields.Many2many(
|
||||
'account.analytic.tag',
|
||||
string='Analytic Tags')
|
||||
move_line_auto_post = fields.Boolean('Auto-post lines', default=True)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
if 'company_id' not in fields:
|
||||
company_id = self.env.user.company_id.id
|
||||
else:
|
||||
company_id = res['company_id']
|
||||
default_journal = self.env['account.journal'].search([
|
||||
('type', '=', 'general'),
|
||||
('company_id', '=', company_id)
|
||||
], limit=1)
|
||||
if 'journal_id' not in res and default_journal:
|
||||
res['journal_id'] = default_journal.id
|
||||
return res
|
||||
|
||||
@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.invoice_id')
|
||||
def _compute_invoice_line(self):
|
||||
for spread in self:
|
||||
invoice_lines = spread.invoice_line_ids
|
||||
line = invoice_lines and invoice_lines[0] or False
|
||||
spread.invoice_line_id = line
|
||||
|
||||
@api.multi
|
||||
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', 'invoice_line_id.price_subtotal',
|
||||
'line_ids.move_id.amount', 'line_ids.move_id.state')
|
||||
def _compute_amounts(self):
|
||||
for spread in self:
|
||||
moves_amount = 0.0
|
||||
posted_amount = 0.0
|
||||
total_amount = spread.estimated_amount
|
||||
if spread.invoice_line_id:
|
||||
total_amount = spread.invoice_line_id.price_subtotal
|
||||
for spread_line in spread.line_ids:
|
||||
if spread_line.move_id:
|
||||
moves_amount += spread_line.amount
|
||||
if spread_line.move_id.state == 'posted':
|
||||
posted_amount += spread_line.amount
|
||||
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.multi
|
||||
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'
|
||||
|
||||
@api.onchange('invoice_type', 'company_id')
|
||||
def onchange_invoice_type(self):
|
||||
company = self.company_id
|
||||
if not self.env.context.get('default_journal_id'):
|
||||
journal = company.default_spread_expense_journal_id
|
||||
if self.invoice_type in ('out_invoice', 'in_refund'):
|
||||
journal = company.default_spread_revenue_journal_id
|
||||
if journal:
|
||||
self.journal_id = journal
|
||||
|
||||
if not self.env.context.get('default_debit_account_id'):
|
||||
if self.invoice_type in ('out_invoice', 'in_refund'):
|
||||
debit_account_id = company.default_spread_revenue_account_id
|
||||
self.debit_account_id = debit_account_id
|
||||
|
||||
if not self.env.context.get('default_credit_account_id'):
|
||||
if self.invoice_type in ('in_invoice', 'out_refund'):
|
||||
credit_account_id = company.default_spread_expense_account_id
|
||||
self.credit_account_id = credit_account_id
|
||||
|
||||
@api.constrains('invoice_id', 'invoice_type')
|
||||
def _check_invoice_type(self):
|
||||
for spread in self:
|
||||
if not spread.invoice_id:
|
||||
pass
|
||||
elif spread.invoice_type != spread.invoice_id.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):
|
||||
raise ValidationError(_(
|
||||
'The Journal is not consistent with the account moves.'))
|
||||
|
||||
@api.constrains('template_id', 'invoice_type')
|
||||
def _check_template_invoice_type(self):
|
||||
for spread in self:
|
||||
if spread.invoice_type in ['in_invoice', 'in_refund']:
|
||||
if spread.template_id.spread_type == 'sale':
|
||||
raise ValidationError(_(
|
||||
'The Spread Template (Sales) is not compatible '
|
||||
'with selected invoice type'))
|
||||
elif spread.invoice_type in ['out_invoice', 'out_refund']:
|
||||
if spread.template_id.spread_type == 'purchase':
|
||||
raise ValidationError(_(
|
||||
'The Spread Template (Purchases) is not compatible '
|
||||
'with selected invoice type'))
|
||||
|
||||
@api.multi
|
||||
def _compute_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
|
||||
|
||||
@api.multi
|
||||
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 = fields.Date.from_string(posted_line_ids[-1].date)
|
||||
months = self._compute_spread_period_duration()
|
||||
spread_date = last_date + relativedelta(months=months)
|
||||
else:
|
||||
spread_date = fields.Date.from_string(self.spread_date)
|
||||
return spread_date
|
||||
|
||||
@api.multi
|
||||
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._compute_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
|
||||
|
||||
@api.multi
|
||||
def _compute_spread_board(self):
|
||||
"""Creates the spread lines. This method is highly inspired
|
||||
from method compute_depreciation_board() present in standard
|
||||
"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
|
||||
amount = self._compute_board_amount(
|
||||
sequence, unposted_amount, number_of_periods
|
||||
)
|
||||
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': self._get_last_day_of_month(spread_date),
|
||||
}
|
||||
commands.append((0, False, vals))
|
||||
|
||||
spread_date = self._next_line_date(month_day, spread_date)
|
||||
|
||||
self.write({'line_ids': commands})
|
||||
self.message_post(body=_("Spread table created."))
|
||||
|
||||
@api.multi
|
||||
def _get_number_of_periods(self, month_day):
|
||||
"""Calculates the number of spread lines."""
|
||||
self.ensure_one()
|
||||
if month_day != 1:
|
||||
return self.period_number + 1
|
||||
return self.period_number
|
||||
|
||||
@staticmethod
|
||||
def _get_last_day_of_month(spread_date):
|
||||
return fields.Date.to_string(spread_date + relativedelta(day=31))
|
||||
|
||||
@api.multi
|
||||
def _compute_board_amount(self, sequence, amount, number_of_periods):
|
||||
"""Calculates the amount for the spread lines."""
|
||||
self.ensure_one()
|
||||
amount_to_spread = self.total_amount
|
||||
if sequence != number_of_periods:
|
||||
amount = amount_to_spread / self.period_number
|
||||
if sequence == 1:
|
||||
date = fields.Datetime.from_string(self.spread_date)
|
||||
month_days = calendar.monthrange(date.year, date.month)[1]
|
||||
days = month_days - date.day + 1
|
||||
period = self.period_number
|
||||
amount = (amount_to_spread / period) / month_days * days
|
||||
return amount
|
||||
|
||||
@api.multi
|
||||
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:
|
||||
if spread.total_amount < 0.0:
|
||||
raise UserError(
|
||||
_("Cannot spread negative amounts of invoice lines"))
|
||||
if spread.total_amount:
|
||||
spread._compute_spread_board()
|
||||
|
||||
@api.multi
|
||||
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()
|
||||
|
||||
@api.multi
|
||||
def action_undo_spread(self):
|
||||
"""Undo spreading: Remove all created moves,
|
||||
restore original account on move line"""
|
||||
self.ensure_one()
|
||||
self.mapped('line_ids').filtered('move_id').unlink_move()
|
||||
self.mapped('line_ids').unlink()
|
||||
|
||||
@api.multi
|
||||
def action_unlink_invoice_line(self):
|
||||
"""Unlink the invoice line from the spread board"""
|
||||
self.ensure_one()
|
||||
if self.invoice_id.state != 'draft':
|
||||
raise UserError(
|
||||
_("Cannot unlink invoice lines if the invoice is validated"))
|
||||
self._action_unlink_invoice_line()
|
||||
|
||||
@api.multi
|
||||
def _action_unlink_invoice_line(self):
|
||||
for spread in self:
|
||||
spread_mls = spread.line_ids.mapped('move_id.line_ids')
|
||||
spread_mls.remove_move_reconcile()
|
||||
spread.write({'invoice_line_ids': [(5, 0, 0)]})
|
||||
|
||||
@api.multi
|
||||
def reconcile_spread_moves(self):
|
||||
for spread in self:
|
||||
spread._reconcile_spread_moves()
|
||||
|
||||
@api.multi
|
||||
def _reconcile_spread_moves(self, created_moves=False):
|
||||
"""Reconcile spread moves if possible"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.invoice_id.number:
|
||||
return
|
||||
|
||||
spread_mls = self.line_ids.mapped('move_id.line_ids')
|
||||
if created_moves:
|
||||
spread_mls |= created_moves.mapped('line_ids')
|
||||
if self.invoice_type in ('in_invoice', 'out_refund'):
|
||||
spread_mls = spread_mls.filtered(lambda x: x.credit != 0.)
|
||||
else:
|
||||
spread_mls = spread_mls.filtered(lambda x: x.debit != 0.)
|
||||
|
||||
invoice_mls = self.invoice_id.move_id.mapped('line_ids')
|
||||
if self.invoice_id.type in ('in_invoice', 'out_refund'):
|
||||
invoice_mls = invoice_mls.filtered(lambda x: x.debit != 0.)
|
||||
else:
|
||||
invoice_mls = invoice_mls.filtered(lambda x: x.credit != 0.)
|
||||
|
||||
to_be_reconciled = self.env['account.move.line']
|
||||
if len(invoice_mls) > 1:
|
||||
# Refine selection of move line.
|
||||
# The name is formatted the same way as it is done when creating
|
||||
# move lines in method "def invoice_line_move_line_get()" of
|
||||
# standard account module
|
||||
raw_name = self.invoice_line_id.name
|
||||
formatted_name = raw_name.split('\n')[0][:64]
|
||||
for move_line in invoice_mls:
|
||||
if move_line.name == formatted_name:
|
||||
to_be_reconciled |= move_line
|
||||
else:
|
||||
to_be_reconciled = invoice_mls
|
||||
|
||||
if len(to_be_reconciled) == 1:
|
||||
do_reconcile = spread_mls + to_be_reconciled
|
||||
do_reconcile.remove_move_reconcile()
|
||||
do_reconcile.reconcile()
|
||||
|
||||
@api.multi
|
||||
def _compute_deprecated_accounts(self):
|
||||
for spread in self:
|
||||
debit_deprecated = bool(spread.debit_account_id.deprecated)
|
||||
credit_deprecated = bool(spread.credit_account_id.deprecated)
|
||||
spread.is_debit_account_deprecated = debit_deprecated
|
||||
spread.is_credit_account_deprecated = credit_deprecated
|
||||
|
||||
@api.multi
|
||||
def open_reconcile_view(self):
|
||||
self.ensure_one()
|
||||
spread_mls = self.line_ids.mapped('move_id.line_ids')
|
||||
return spread_mls.open_reconcile_view()
|
150
account_spread_cost_revenue/models/account_spread_line.py
Normal file
150
account_spread_cost_revenue/models/account_spread_line.py
Normal file
@ -0,0 +1,150 @@
|
||||
# Copyright 2016-2018 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.addons import decimal_precision as dp
|
||||
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=dp.get_precision('Account'), required=True)
|
||||
date = fields.Date(required=True)
|
||||
spread_id = fields.Many2one(
|
||||
'account.spread', string='Spread Board', ondelete='cascade')
|
||||
move_id = fields.Many2one(
|
||||
'account.move', string='Journal Entry', readonly=True)
|
||||
|
||||
@api.multi
|
||||
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(str(id) for id in created_moves.ids)
|
||||
spread.message_post(body=post_msg)
|
||||
|
||||
spread._reconcile_spread_moves(created_moves)
|
||||
if created_moves and spread.move_line_auto_post:
|
||||
created_moves.post()
|
||||
|
||||
@api.multi
|
||||
def create_move(self):
|
||||
"""Button to manually create a move from a spread line entry.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.create_and_reconcile_moves()
|
||||
|
||||
@api.multi
|
||||
def _create_moves(self):
|
||||
created_moves = self.env['account.move']
|
||||
for line in self:
|
||||
if line.move_id:
|
||||
raise UserError(_('This spread line is already linked to a '
|
||||
'journal entry! Please post or delete it.'))
|
||||
|
||||
move_vals = line._prepare_move()
|
||||
move = self.env['account.move'].create(move_vals)
|
||||
|
||||
line.write({'move_id': move.id})
|
||||
created_moves += move
|
||||
return created_moves
|
||||
|
||||
@api.multi
|
||||
def _prepare_move(self):
|
||||
self.ensure_one()
|
||||
|
||||
spread_date = self.env.context.get('spread_date') or self.date
|
||||
spread = self.spread_id
|
||||
analytic = spread.account_analytic_id
|
||||
analytic_tags = [(4, tag.id, None) for tag in spread.analytic_tag_ids]
|
||||
|
||||
company_currency = spread.company_id.currency_id
|
||||
current_currency = spread.currency_id
|
||||
not_same_curr = company_currency != current_currency
|
||||
amount = current_currency.with_context(date=spread_date).compute(
|
||||
self.amount, company_currency)
|
||||
|
||||
line_ids = [(0, 0, {
|
||||
'name': spread.name.split('\n')[0][:64],
|
||||
'account_id': spread.debit_account_id.id,
|
||||
'debit': amount,
|
||||
'credit': 0.0,
|
||||
'analytic_account_id': analytic.id,
|
||||
'analytic_tag_ids': analytic_tags,
|
||||
'currency_id': not_same_curr and current_currency.id or False,
|
||||
'amount_currency': not_same_curr and - 1.0 * self.amount or 0.0,
|
||||
}), (0, 0, {
|
||||
'name': spread.name.split('\n')[0][:64],
|
||||
'account_id': spread.credit_account_id.id,
|
||||
'credit': amount,
|
||||
'debit': 0.0,
|
||||
'analytic_account_id': analytic.id,
|
||||
'analytic_tag_ids': analytic_tags,
|
||||
'currency_id': not_same_curr and current_currency.id or False,
|
||||
'amount_currency': not_same_curr and self.amount or 0.0,
|
||||
})]
|
||||
|
||||
return {
|
||||
'name': self.spread_id and self.spread_id.name or "/",
|
||||
'ref': self.name,
|
||||
'date': spread_date,
|
||||
'journal_id': spread.journal_id.id,
|
||||
'line_ids': line_ids,
|
||||
'company_id': spread.company_id.id,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
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_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'account.move',
|
||||
'view_id': False,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': self.move_id.id,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
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.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()
|
@ -0,0 +1,78 @@
|
||||
# Copyright 2018 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountSpreadTemplate(models.Model):
|
||||
_name = 'account.spread.template'
|
||||
_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.user.company_id,
|
||||
string='Company',
|
||||
required=True)
|
||||
spread_journal_id = fields.Many2one(
|
||||
'account.journal',
|
||||
string='Journal',
|
||||
required=True)
|
||||
spread_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='Spread Balance Sheet Account',
|
||||
required=True)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
if 'company_id' not in fields:
|
||||
company_id = self.env.user.company_id.id
|
||||
else:
|
||||
company_id = res['company_id']
|
||||
default_journal = self.env['account.journal'].search([
|
||||
('type', '=', 'general'),
|
||||
('company_id', '=', company_id)],
|
||||
limit=1)
|
||||
if 'spread_journal_id' not in res and default_journal:
|
||||
res['spread_journal_id'] = default_journal.id
|
||||
return res
|
||||
|
||||
@api.onchange('spread_type', 'company_id')
|
||||
def onchange_spread_type(self):
|
||||
company = self.company_id
|
||||
if self.spread_type == 'sale':
|
||||
account = company.default_spread_revenue_account_id
|
||||
journal = company.default_spread_revenue_journal_id
|
||||
else:
|
||||
account = company.default_spread_expense_account_id
|
||||
journal = company.default_spread_expense_journal_id
|
||||
if account:
|
||||
self.spread_account_id = account
|
||||
if journal:
|
||||
self.spread_journal_id = journal
|
||||
|
||||
def _prepare_spread_from_template(self):
|
||||
self.ensure_one()
|
||||
company = self.company_id
|
||||
spread_vals = {
|
||||
'name': self.name,
|
||||
'template_id': self.id,
|
||||
'journal_id': self.spread_journal_id.id,
|
||||
'company_id': company.id,
|
||||
}
|
||||
|
||||
if self.spread_type == 'sale':
|
||||
invoice_type = 'out_invoice'
|
||||
spread_vals['debit_account_id'] = self.spread_account_id.id
|
||||
else:
|
||||
invoice_type = 'in_invoice'
|
||||
spread_vals['credit_account_id'] = self.spread_account_id.id
|
||||
|
||||
spread_vals['invoice_type'] = invoice_type
|
||||
return spread_vals
|
20
account_spread_cost_revenue/models/res_company.py
Normal file
20
account_spread_cost_revenue/models/res_company.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Copyright 2018 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')
|
12
account_spread_cost_revenue/readme/CONFIGURE.rst
Normal file
12
account_spread_cost_revenue/readme/CONFIGURE.rst
Normal file
@ -0,0 +1,12 @@
|
||||
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*.
|
1
account_spread_cost_revenue/readme/CONTRIBUTORS.rst
Normal file
1
account_spread_cost_revenue/readme/CONTRIBUTORS.rst
Normal file
@ -0,0 +1 @@
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
3
account_spread_cost_revenue/readme/CREDITS.rst
Normal file
3
account_spread_cost_revenue/readme/CREDITS.rst
Normal file
@ -0,0 +1,3 @@
|
||||
Part of the code in this module (in particular the computation of the spread lines)
|
||||
is highly inspired by the Assets Management module from the standard
|
||||
Odoo 11.0 Community developed by Odoo SA.
|
1
account_spread_cost_revenue/readme/DESCRIPTION.rst
Normal file
1
account_spread_cost_revenue/readme/DESCRIPTION.rst
Normal file
@ -0,0 +1 @@
|
||||
Allows to spread costs or revenues over a customizable periods, to even out cost or invoice spikes.
|
5
account_spread_cost_revenue/readme/HISTORY.rst
Normal file
5
account_spread_cost_revenue/readme/HISTORY.rst
Normal file
@ -0,0 +1,5 @@
|
||||
11.0.1.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [ADD] Module account_spread_cost_revenue.
|
||||
(`#715 <https://github.com/OCA/account-financial-tools/pull/715>`_)
|
2
account_spread_cost_revenue/readme/ROADMAP.rst
Normal file
2
account_spread_cost_revenue/readme/ROADMAP.rst
Normal file
@ -0,0 +1,2 @@
|
||||
* Verify last day of month
|
||||
* Add help in fields definition
|
74
account_spread_cost_revenue/readme/USAGE.rst
Normal file
74
account_spread_cost_revenue/readme/USAGE.rst
Normal file
@ -0,0 +1,74 @@
|
||||
Define Spread Costs/Revenues Board
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Under Invoicing -> Adviser -> Accounting Entries -> 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/11.0/account_spread_cost_revenue/static/description/spread.png
|
||||
:alt: Create a new spread board
|
||||
|
||||
Click on the button on the top-left to calculate the spread lines.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/11.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/11.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.
|
||||
|
||||
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/11.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/11.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 *Reconciled entries*: the moves of the spread lines are reconciled 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*
|
||||
* *Journal*
|
||||
|
||||
When creating a new Spread Costs/Revenues Board, select the right template.
|
||||
This way the above fields will be copied to the Spread Board.
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data 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','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
7
account_spread_cost_revenue/security/ir.model.access.csv
Normal file
7
account_spread_cost_revenue/security/ir.model.access.csv
Normal file
@ -0,0 +1,7 @@
|
||||
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_user,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_user,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_user,1,0,0,0
|
|
BIN
account_spread_cost_revenue/static/description/create_spread.png
Normal file
BIN
account_spread_cost_revenue/static/description/create_spread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
account_spread_cost_revenue/static/description/icon.png
Normal file
BIN
account_spread_cost_revenue/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
503
account_spread_cost_revenue/static/description/index.html
Normal file
503
account_spread_cost_revenue/static/description/index.html
Normal file
@ -0,0 +1,503 @@
|
||||
<?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 0.14: http://docutils.sourceforge.net/" />
|
||||
<title>Cost-Revenue Spread</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See http://docutils.sf.net/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. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external" 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" 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" href="https://github.com/OCA/account-financial-tools/tree/11.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" href="https://translation.odoo-community.org/projects/account-financial-tools-11-0/account-financial-tools-11-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" href="https://runbot.odoo-community.org/runbot/92/11.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-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="id1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#known-issues-roadmap" id="id3">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="id5">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="id6">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="id7">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#other-credits" id="id8">Other credits</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="id9">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#id1">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>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#id2">Usage</a></h1>
|
||||
<p>Under Invoicing -> Adviser -> Accounting Entries -> 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/11.0/account_spread_cost_revenue/static/description/spread.png" />
|
||||
</div>
|
||||
<p>Click on the 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/11.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/11.0/account_spread_cost_revenue/static/description/update_spread.png" />
|
||||
</div>
|
||||
<p>By default, the status of the created accounting moves is unposted, so you should post them manually one by one.
|
||||
To allow the automatic posting of the accounting moves, set the flag <em>Auto-post lines</em> to True.</p>
|
||||
<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/11.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/11.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>Reconciled entries</em>: the moves of the spread lines are reconciled 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>
|
||||
<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="known-issues-roadmap">
|
||||
<h1><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h1>
|
||||
<ul class="simple">
|
||||
<li>Verify last day of month</li>
|
||||
<li>Not yet compatible with cutoff module: create module for adaptation?</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#id4">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 smashing 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:%2011.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="#id5">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Onestein</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#id7">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Andrea Stirpe <<a class="reference external" href="mailto:a.stirpe@onestein.nl">a.stirpe@onestein.nl</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="other-credits">
|
||||
<h2><a class="toc-backref" href="#id8">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="#id9">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>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
|
||||
<p><a class="reference external" href="https://github.com/astirpe"><img alt="astirpe" src="https://github.com/astirpe.png?size=40px" /></a></p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/11.0/account_spread_cost_revenue">OCA/account-financial-tools</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
account_spread_cost_revenue/static/description/spread.png
Normal file
BIN
account_spread_cost_revenue/static/description/spread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
account_spread_cost_revenue/static/description/update_spread.png
Normal file
BIN
account_spread_cost_revenue/static/description/update_spread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
62
account_spread_cost_revenue/static/src/js/account_spread.js
Normal file
62
account_spread_cost_revenue/static/src/js/account_spread.js
Normal file
@ -0,0 +1,62 @@
|
||||
odoo.define('account_spread_cost_revenue.widget', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var core = require('web.core');
|
||||
var registry = require('web.field_registry');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var AccountSpreadWidget = AbstractField.extend({
|
||||
events: _.extend({}, AbstractField.prototype.events, {
|
||||
'click': '_onClick',
|
||||
}),
|
||||
description: "",
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
isSet: function () {
|
||||
return this.value !== 'unavailable';
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_render: function () {
|
||||
var className = '';
|
||||
var style = 'btn fa fa-arrow-circle-right o_spread_line ';
|
||||
var title = '';
|
||||
if (this.recordData.spread_check === 'linked') {
|
||||
className = 'o_is_linked';
|
||||
title = _t('Linked to spread');
|
||||
} else {
|
||||
title = _t('Not linked to spread');
|
||||
}
|
||||
var $button = $('<button/>', {
|
||||
type: 'button',
|
||||
title: title,
|
||||
}).addClass(style + className);
|
||||
this.$el.html($button);
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onClick: function (event) {
|
||||
event.stopPropagation();
|
||||
this.trigger_up('button_clicked', {
|
||||
attrs: {
|
||||
name: 'spread_details',
|
||||
type: 'object',
|
||||
},
|
||||
record: this.record,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registry.add("spread_line_widget", AccountSpreadWidget);
|
||||
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
.o_web_client .o_spread_line {
|
||||
&.o_is_linked {
|
||||
color: @brand-success;
|
||||
}
|
||||
}
|
11
account_spread_cost_revenue/templates/assets.xml
Normal file
11
account_spread_cost_revenue/templates/assets.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="assets_backend" name="account spread" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/account_spread_cost_revenue/static/src/less/account_spread.less"/>
|
||||
<script type="text/javascript" src="/account_spread_cost_revenue/static/src/js/account_spread.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
5
account_spread_cost_revenue/tests/__init__.py
Normal file
5
account_spread_cost_revenue/tests/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# 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
|
726
account_spread_cost_revenue/tests/test_account_invoice_spread.py
Normal file
726
account_spread_cost_revenue/tests/test_account_invoice_spread.py
Normal file
@ -0,0 +1,726 @@
|
||||
# Copyright 2018 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.tools import convert_file
|
||||
from odoo.modules.module import get_module_resource
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
|
||||
|
||||
|
||||
class TestAccountInvoiceSpread(AccountingTestCase):
|
||||
|
||||
def _load(self, module, *args):
|
||||
convert_file(
|
||||
self.cr,
|
||||
'account_spread_cost_revenue',
|
||||
get_module_resource(module, *args),
|
||||
{}, 'init', False, 'test', self.registry._assertion_report)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._load('account', 'test', 'account_minimal_test.xml')
|
||||
|
||||
type_receivable = self.env.ref('account.data_account_type_receivable')
|
||||
type_payable = self.env.ref('account.data_account_type_payable')
|
||||
|
||||
def get_account(obj):
|
||||
return self.env['account.account'].search([
|
||||
('user_type_id', '=', obj.id),
|
||||
('reconcile', '=', True),
|
||||
], limit=1)
|
||||
|
||||
self.invoice_account = get_account(type_receivable)
|
||||
self.invoice_line_account = get_account(type_payable)
|
||||
|
||||
self.spread_account = self.env['account.account'].search([
|
||||
('user_type_id', '=', type_payable.id),
|
||||
('id', '!=', self.invoice_line_account.id)
|
||||
], limit=1)
|
||||
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Partner Name',
|
||||
'supplier': True,
|
||||
})
|
||||
self.invoice = self.env['account.invoice'].with_context(
|
||||
default_type='in_invoice'
|
||||
).create({
|
||||
'partner_id': partner.id,
|
||||
'account_id': self.invoice_account.id,
|
||||
'type': 'in_invoice',
|
||||
})
|
||||
self.invoice_line = self.env['account.invoice.line'].create({
|
||||
'quantity': 1.0,
|
||||
'price_unit': 1000.0,
|
||||
'invoice_id': self.invoice.id,
|
||||
'name': 'product that cost 1000',
|
||||
'account_id': self.invoice_account.id,
|
||||
})
|
||||
|
||||
analytic_tags = [(6, 0, self.env.ref('analytic.tag_contract').ids)]
|
||||
self.analytic_account = self.env['account.analytic.account'].create({
|
||||
'name': 'test account',
|
||||
})
|
||||
self.spread = self.env['account.spread'].create({
|
||||
'name': 'test',
|
||||
'debit_account_id': self.spread_account.id,
|
||||
'credit_account_id': self.invoice_line_account.id,
|
||||
'period_number': 12,
|
||||
'period_type': 'month',
|
||||
'spread_date': '2017-02-01',
|
||||
'estimated_amount': 1000.0,
|
||||
'journal_id': self.invoice.journal_id.id,
|
||||
'invoice_type': 'in_invoice',
|
||||
'account_analytic_id': self.analytic_account.id,
|
||||
'analytic_tag_ids': analytic_tags,
|
||||
})
|
||||
|
||||
self.invoice_2 = self.env['account.invoice'].with_context(
|
||||
default_type='out_invoice'
|
||||
).create({
|
||||
'partner_id': partner.id,
|
||||
'account_id': self.invoice_account.id,
|
||||
'type': 'out_invoice',
|
||||
})
|
||||
self.invoice_line_2 = self.env['account.invoice.line'].create({
|
||||
'quantity': 1.0,
|
||||
'price_unit': 1000.0,
|
||||
'invoice_id': self.invoice_2.id,
|
||||
'name': 'product that cost 1000',
|
||||
'account_id': self.invoice_line_account.id,
|
||||
})
|
||||
self.spread2 = self.env['account.spread'].create({
|
||||
'name': 'test2',
|
||||
'debit_account_id': self.spread_account.id,
|
||||
'credit_account_id': self.invoice_line_account.id,
|
||||
'period_number': 12,
|
||||
'period_type': 'month',
|
||||
'spread_date': '2017-02-01',
|
||||
'estimated_amount': 1000.0,
|
||||
'journal_id': self.invoice_2.journal_id.id,
|
||||
'invoice_type': 'out_invoice',
|
||||
})
|
||||
|
||||
def test_01_wizard_defaults(self):
|
||||
my_company = self.env.user.company_id
|
||||
Wizard = self.env['account.spread.invoice.line.link.wizard']
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=my_company.id,
|
||||
).create({})
|
||||
|
||||
self.assertEqual(wizard1.invoice_line_id, self.invoice_line)
|
||||
self.assertEqual(wizard1.invoice_line_id.invoice_id, self.invoice)
|
||||
self.assertEqual(wizard1.invoice_type, 'in_invoice')
|
||||
self.assertFalse(wizard1.spread_id)
|
||||
self.assertEqual(wizard1.company_id, my_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_2.id,
|
||||
default_company_id=my_company.id,
|
||||
).create({})
|
||||
|
||||
self.assertEqual(wizard2.invoice_line_id, self.invoice_line_2)
|
||||
self.assertEqual(wizard2.invoice_line_id.invoice_id, self.invoice_2)
|
||||
self.assertEqual(wizard2.invoice_type, 'out_invoice')
|
||||
self.assertFalse(wizard2.spread_id)
|
||||
self.assertEqual(wizard2.company_id, my_company)
|
||||
self.assertEqual(wizard2.spread_action_type, 'link')
|
||||
self.assertFalse(wizard2.spread_account_id)
|
||||
self.assertFalse(wizard2.spread_journal_id)
|
||||
|
||||
def test_02_wizard_defaults(self):
|
||||
my_company = self.env.user.company_id
|
||||
Wizard = self.env['account.spread.invoice.line.link.wizard']
|
||||
|
||||
account_revenue = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_revenue').id)],
|
||||
limit=1)
|
||||
account_payable = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_payable').id)],
|
||||
limit=1)
|
||||
exp_journal = self.ref('account_spread_cost_revenue.expenses_journal')
|
||||
sales_journal = self.ref('account_spread_cost_revenue.sales_journal')
|
||||
my_company.default_spread_revenue_account_id = account_revenue
|
||||
my_company.default_spread_expense_account_id = account_payable
|
||||
my_company.default_spread_revenue_journal_id = sales_journal
|
||||
my_company.default_spread_expense_journal_id = exp_journal
|
||||
|
||||
self.assertTrue(my_company.default_spread_revenue_account_id)
|
||||
self.assertTrue(my_company.default_spread_expense_account_id)
|
||||
self.assertTrue(my_company.default_spread_revenue_journal_id)
|
||||
self.assertTrue(my_company.default_spread_expense_journal_id)
|
||||
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=my_company.id,
|
||||
).create({})
|
||||
|
||||
self.assertEqual(wizard1.invoice_line_id, self.invoice_line)
|
||||
self.assertEqual(wizard1.invoice_line_id.invoice_id, self.invoice)
|
||||
self.assertEqual(wizard1.invoice_type, 'in_invoice')
|
||||
self.assertFalse(wizard1.spread_id)
|
||||
self.assertEqual(wizard1.company_id, my_company)
|
||||
self.assertEqual(wizard1.spread_action_type, 'link')
|
||||
self.assertFalse(wizard1.spread_account_id)
|
||||
self.assertFalse(wizard1.spread_journal_id)
|
||||
|
||||
res_onchange = wizard1.onchange_invoice_type()
|
||||
self.assertTrue(res_onchange)
|
||||
self.assertTrue(res_onchange.get('domain'))
|
||||
|
||||
wizard1._onchange_spread_journal_account()
|
||||
self.assertTrue(wizard1.spread_account_id)
|
||||
self.assertTrue(wizard1.spread_journal_id)
|
||||
self.assertEqual(wizard1.spread_account_id, account_payable)
|
||||
self.assertEqual(wizard1.spread_journal_id.id, exp_journal)
|
||||
|
||||
wizard2 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line_2.id,
|
||||
default_company_id=my_company.id,
|
||||
).create({})
|
||||
|
||||
self.assertEqual(wizard2.invoice_line_id, self.invoice_line_2)
|
||||
self.assertEqual(wizard2.invoice_line_id.invoice_id, self.invoice_2)
|
||||
self.assertEqual(wizard2.invoice_type, 'out_invoice')
|
||||
self.assertFalse(wizard2.spread_id)
|
||||
self.assertEqual(wizard2.company_id, my_company)
|
||||
self.assertEqual(wizard2.spread_action_type, 'link')
|
||||
self.assertFalse(wizard2.spread_account_id)
|
||||
self.assertFalse(wizard2.spread_journal_id)
|
||||
|
||||
res_onchange = wizard2.onchange_invoice_type()
|
||||
self.assertTrue(res_onchange)
|
||||
self.assertTrue(res_onchange.get('domain'))
|
||||
|
||||
wizard2._onchange_spread_journal_account()
|
||||
self.assertTrue(wizard2.spread_account_id)
|
||||
self.assertTrue(wizard2.spread_journal_id)
|
||||
self.assertEqual(wizard2.spread_account_id, account_revenue)
|
||||
self.assertEqual(wizard2.spread_journal_id.id, sales_journal)
|
||||
|
||||
def test_03_link_invoice_line_with_spread_sheet(self):
|
||||
|
||||
my_company = self.env.user.company_id
|
||||
Wizard = self.env['account.spread.invoice.line.link.wizard']
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=my_company.id,
|
||||
).create({})
|
||||
self.assertEqual(wizard1.spread_action_type, 'link')
|
||||
|
||||
wizard1.spread_account_id = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_revenue').id)],
|
||||
limit=1)
|
||||
wizard1.spread_journal_id = self.ref(
|
||||
'account_spread_cost_revenue.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.invoice_line)
|
||||
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.invoice.journal_id.update_posted = True
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
self.assertTrue(line.move_id.journal_id.update_posted)
|
||||
for ml in line.move_id.line_ids:
|
||||
ml_analytic_account = ml.analytic_account_id
|
||||
analytic_tag = self.env.ref('analytic.tag_contract')
|
||||
self.assertEqual(ml_analytic_account, self.analytic_account)
|
||||
self.assertEqual(ml.analytic_tag_ids, analytic_tag)
|
||||
|
||||
self.spread.invoice_id.action_cancel()
|
||||
|
||||
self.assertTrue(self.spread.invoice_line_id)
|
||||
with self.assertRaises(UserError):
|
||||
self.spread.action_unlink_invoice_line()
|
||||
self.assertTrue(self.spread.invoice_line_id)
|
||||
|
||||
def test_04_new_spread_sheet(self):
|
||||
|
||||
my_company = self.env.user.company_id
|
||||
Wizard = self.env['account.spread.invoice.line.link.wizard']
|
||||
|
||||
spread_account = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_revenue').id)],
|
||||
limit=1)
|
||||
spread_journal_id = self.ref(
|
||||
'account_spread_cost_revenue.expenses_journal')
|
||||
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=my_company.id,
|
||||
).create({
|
||||
'spread_action_type': 'new',
|
||||
})
|
||||
self.assertEqual(wizard1.spread_action_type, 'new')
|
||||
|
||||
wizard1.write({
|
||||
'spread_account_id': spread_account.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'))
|
||||
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
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_2.id,
|
||||
default_company_id=my_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'))
|
||||
|
||||
spread_lines = self.spread2.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.spread2.compute_spread_board()
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
def test_05_new_spread_sheet_from_template(self):
|
||||
|
||||
my_company = self.env.user.company_id
|
||||
Wizard = self.env['account.spread.invoice.line.link.wizard']
|
||||
|
||||
spread_account = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_payable').id)],
|
||||
limit=1)
|
||||
self.assertTrue(spread_account)
|
||||
spread_journal_id = self.ref(
|
||||
'account_spread_cost_revenue.expenses_journal')
|
||||
|
||||
template = self.env['account.spread.template'].create({
|
||||
'name': 'test',
|
||||
'spread_type': 'purchase',
|
||||
'spread_account_id': spread_account.id,
|
||||
'spread_journal_id': spread_journal_id,
|
||||
})
|
||||
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=my_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'))
|
||||
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
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_2.id,
|
||||
default_company_id=my_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'))
|
||||
|
||||
spread_lines = self.spread2.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.spread2.compute_spread_board()
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
def test_06_open_wizard(self):
|
||||
|
||||
res_action = self.invoice_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.invoice_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_2.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):
|
||||
self.invoice_line.copy()
|
||||
self.assertEqual(len(self.invoice.invoice_line_ids), 2)
|
||||
|
||||
self.invoice.invoice_line_ids[0].spread_id = self.spread
|
||||
self.assertTrue(self.spread.invoice_line_id)
|
||||
self.assertEqual(self.spread.invoice_line_id, self.invoice_line)
|
||||
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
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.invoice.action_invoice_open()
|
||||
|
||||
def test_09_no_link_invoice(self):
|
||||
|
||||
balance_sheet = self.spread.credit_account_id
|
||||
|
||||
# Validate invoice
|
||||
self.invoice.action_invoice_open()
|
||||
|
||||
invoice_mls = self.invoice.move_id.mapped('line_ids')
|
||||
self.assertTrue(invoice_mls)
|
||||
for invoice_ml in invoice_mls:
|
||||
if invoice_ml.debit:
|
||||
self.assertNotEqual(invoice_ml.account_id, balance_sheet)
|
||||
|
||||
def test_10_link_vendor_bill_line_with_spread_sheet(self):
|
||||
|
||||
copied_line = self.invoice_line.copy()
|
||||
copied_line.name = 'new test line'
|
||||
self.spread.write({
|
||||
'estimated_amount': 1000.0,
|
||||
'period_number': 12,
|
||||
'period_type': 'month',
|
||||
'spread_date': '2017-01-07',
|
||||
'invoice_line_id': self.invoice_line.id,
|
||||
'move_line_auto_post': False,
|
||||
})
|
||||
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
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.invoice.action_invoice_open()
|
||||
|
||||
invoice_mls = self.invoice.move_id.mapped('line_ids')
|
||||
self.assertTrue(invoice_mls)
|
||||
|
||||
count_balance_sheet = 0
|
||||
for invoice_ml in invoice_mls:
|
||||
if invoice_ml.account_id == balance_sheet:
|
||||
count_balance_sheet += 1
|
||||
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.assertTrue(spread_ml.full_reconcile_id)
|
||||
|
||||
action_reconcile_view = self.spread2.open_reconcile_view()
|
||||
self.assertTrue(isinstance(action_reconcile_view, dict))
|
||||
self.assertFalse(action_reconcile_view.get('domain')[0][2])
|
||||
self.assertTrue(action_reconcile_view.get('context'))
|
||||
|
||||
def test_11_link_vendor_bill_line_with_spread_sheet(self):
|
||||
self.invoice_line.copy()
|
||||
self.spread.write({
|
||||
'estimated_amount': 1000.0,
|
||||
'period_number': 12,
|
||||
'period_type': 'month',
|
||||
'spread_date': '2017-01-07',
|
||||
'invoice_line_id': self.invoice_line.id,
|
||||
'move_line_auto_post': False,
|
||||
})
|
||||
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
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.invoice.action_invoice_open()
|
||||
|
||||
invoice_mls = self.invoice.move_id.mapped('line_ids')
|
||||
self.assertTrue(invoice_mls)
|
||||
|
||||
count_balance_sheet = 0
|
||||
for invoice_ml in invoice_mls:
|
||||
if invoice_ml.account_id == balance_sheet:
|
||||
count_balance_sheet += 1
|
||||
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:
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
|
||||
action_reconcile_view = self.spread.open_reconcile_view()
|
||||
self.assertTrue(isinstance(action_reconcile_view, dict))
|
||||
self.assertTrue(action_reconcile_view.get('domain')[0][2])
|
||||
self.assertTrue(action_reconcile_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_12_link_invoice_line_with_spread_sheet_full_reconcile(self):
|
||||
|
||||
self.spread2.write({
|
||||
'estimated_amount': 1000.0,
|
||||
'period_number': 12,
|
||||
'period_type': 'month',
|
||||
'spread_date': '2017-01-07',
|
||||
'invoice_line_id': self.invoice_line_2.id,
|
||||
'move_line_auto_post': False,
|
||||
})
|
||||
|
||||
spread_lines = self.spread2.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
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.spread.credit_account_id
|
||||
balance_sheet = self.spread.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)
|
||||
if spread_ml.credit:
|
||||
self.assertEqual(spread_ml.account_id, payable_account)
|
||||
|
||||
# Validate invoice
|
||||
self.invoice_2.action_invoice_open()
|
||||
|
||||
invoice_mls = self.invoice_2.move_id.mapped('line_ids')
|
||||
self.assertTrue(invoice_mls)
|
||||
for invoice_ml in invoice_mls:
|
||||
if invoice_ml.credit:
|
||||
self.assertEqual(invoice_ml.account_id, balance_sheet)
|
||||
|
||||
self.spread2.line_ids.create_and_reconcile_moves()
|
||||
|
||||
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.assertTrue(spread_ml.full_reconcile_id)
|
||||
if spread_ml.credit:
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
|
||||
action_reconcile_view = self.spread2.open_reconcile_view()
|
||||
self.assertTrue(isinstance(action_reconcile_view, dict))
|
||||
self.assertTrue(action_reconcile_view.get('domain')[0][2])
|
||||
self.assertFalse(action_reconcile_view.get('res_id'))
|
||||
self.assertTrue(action_reconcile_view.get('context'))
|
||||
|
||||
action_spread_details = self.invoice_line_2.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': '2017-01-07',
|
||||
})
|
||||
|
||||
self.spread2.compute_spread_board()
|
||||
spread_lines = self.spread2.line_ids
|
||||
self.assertEqual(len(spread_lines), 13)
|
||||
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
spread_lines[0]._create_moves().post()
|
||||
spread_lines[1]._create_moves().post()
|
||||
spread_lines[2]._create_moves().post()
|
||||
spread_lines[3]._create_moves().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')
|
||||
|
||||
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.spread.debit_account_id
|
||||
self.assertTrue(balance_sheet.reconcile)
|
||||
|
||||
self.spread2.write({
|
||||
'invoice_line_id': self.invoice_line_2.id,
|
||||
})
|
||||
|
||||
# Validate invoice
|
||||
self.invoice_2.action_invoice_open()
|
||||
|
||||
invoice_mls = self.invoice_2.move_id.mapped('line_ids')
|
||||
self.assertTrue(invoice_mls)
|
||||
for invoice_ml in invoice_mls:
|
||||
if invoice_ml.credit:
|
||||
self.assertEqual(invoice_ml.account_id, balance_sheet)
|
||||
|
||||
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.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'].search([
|
||||
('id', '!=', self.invoice_2.journal_id.id),
|
||||
], limit=1)
|
||||
self.assertTrue(other_journal)
|
||||
with self.assertRaises(ValidationError):
|
||||
self.spread2.journal_id = other_journal
|
@ -0,0 +1,351 @@
|
||||
# Copyright 2018 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import time
|
||||
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
from odoo.tools import convert_file, mute_logger
|
||||
from odoo.modules.module import get_module_resource
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
|
||||
|
||||
|
||||
class TestAccountSpreadCostRevenue(AccountingTestCase):
|
||||
|
||||
def _load(self, module, *args):
|
||||
convert_file(
|
||||
self.cr,
|
||||
'account_spread_cost_revenue',
|
||||
get_module_resource(module, *args),
|
||||
{}, 'init', False, 'test', self.registry._assertion_report)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._load('account', 'test', 'account_minimal_test.xml')
|
||||
|
||||
def get_account(obj):
|
||||
return self.env['account.account'].search([
|
||||
('user_type_id', '=', obj.id)
|
||||
], limit=1)
|
||||
|
||||
type_receivable = self.env.ref('account.data_account_type_receivable')
|
||||
type_expenses = self.env.ref('account.data_account_type_expenses')
|
||||
|
||||
self.credit_account = get_account(type_receivable)
|
||||
self.debit_account = get_account(type_expenses)
|
||||
|
||||
def test_01_account_spread_defaults(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,
|
||||
})
|
||||
my_company = self.env.user.company_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.account_analytic_id)
|
||||
self.assertFalse(spread.analytic_tag_ids)
|
||||
self.assertTrue(spread.move_line_auto_post)
|
||||
self.assertEqual(spread.name, 'test')
|
||||
self.assertEqual(spread.invoice_type, 'out_invoice')
|
||||
self.assertEqual(spread.company_id, my_company)
|
||||
self.assertEqual(spread.currency_id, my_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.)
|
||||
self.assertEqual(spread.unposted_amount, 0.)
|
||||
self.assertEqual(spread.total_amount, 0.)
|
||||
self.assertEqual(spread.estimated_amount, 0.)
|
||||
self.assertEqual(spread.spread_date, time.strftime('%Y-01-01'))
|
||||
self.assertTrue(spread.journal_id)
|
||||
self.assertEqual(spread.journal_id.type, 'general')
|
||||
|
||||
def test_02_config_defaults(self):
|
||||
my_company = self.env.user.company_id
|
||||
self.assertFalse(my_company.default_spread_revenue_account_id)
|
||||
self.assertFalse(my_company.default_spread_expense_account_id)
|
||||
self.assertFalse(my_company.default_spread_revenue_journal_id)
|
||||
self.assertFalse(my_company.default_spread_expense_journal_id)
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
def test_03_no_defaults(self):
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.env['account.spread'].create({
|
||||
'name': 'test',
|
||||
})
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
def test_04_no_defaults(self):
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.env['account.spread'].create({
|
||||
'name': 'test',
|
||||
'invoice_type': 'out_invoice',
|
||||
})
|
||||
|
||||
def test_05_config_settings(self):
|
||||
my_company = self.env.user.company_id
|
||||
|
||||
account_revenue = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_revenue').id)],
|
||||
limit=1)
|
||||
account_payable = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_payable').id)],
|
||||
limit=1)
|
||||
exp_journal = self.ref('account_spread_cost_revenue.expenses_journal')
|
||||
sales_journal = self.ref('account_spread_cost_revenue.sales_journal')
|
||||
my_company.default_spread_revenue_account_id = account_revenue
|
||||
my_company.default_spread_expense_account_id = account_payable
|
||||
my_company.default_spread_revenue_journal_id = sales_journal
|
||||
my_company.default_spread_expense_journal_id = exp_journal
|
||||
|
||||
self.assertTrue(my_company.default_spread_revenue_account_id)
|
||||
self.assertTrue(my_company.default_spread_expense_account_id)
|
||||
self.assertTrue(my_company.default_spread_revenue_journal_id)
|
||||
self.assertTrue(my_company.default_spread_expense_journal_id)
|
||||
|
||||
spread = self.env['account.spread'].new({
|
||||
'name': 'test',
|
||||
})
|
||||
|
||||
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.account_analytic_id)
|
||||
self.assertFalse(spread.analytic_tag_ids)
|
||||
self.assertFalse(spread.move_line_auto_post)
|
||||
|
||||
defaults = (self.env['account.spread'].default_get([
|
||||
'company_id',
|
||||
'currency_id',
|
||||
]))
|
||||
|
||||
self.assertEqual(defaults['company_id'], my_company.id)
|
||||
self.assertEqual(defaults['currency_id'], my_company.currency_id.id)
|
||||
|
||||
spread.invoice_type = 'out_invoice'
|
||||
spread.company_id = my_company
|
||||
spread.onchange_invoice_type()
|
||||
self.assertEqual(spread.debit_account_id, account_revenue)
|
||||
self.assertFalse(spread.credit_account_id)
|
||||
self.assertEqual(spread.journal_id.id, sales_journal)
|
||||
self.assertEqual(spread.spread_type, 'sale')
|
||||
|
||||
spread.invoice_type = 'in_invoice'
|
||||
spread.onchange_invoice_type()
|
||||
self.assertEqual(spread.credit_account_id, account_payable)
|
||||
self.assertEqual(spread.journal_id.id, exp_journal)
|
||||
self.assertEqual(spread.spread_type, 'purchase')
|
||||
|
||||
def test_06_invoice_line_compute_spread_check(self):
|
||||
invoice_account = self.env['account.account'].search([
|
||||
('user_type_id', '=', self.env.ref(
|
||||
'account.data_account_type_receivable').id)
|
||||
], limit=1).id
|
||||
invoice_line_account = self.env['account.account'].search([
|
||||
('user_type_id', '=', self.env.ref(
|
||||
'account.data_account_type_expenses').id)
|
||||
], limit=1).id
|
||||
invoice = self.env['account.invoice'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'account_id': invoice_account,
|
||||
'type': 'in_invoice',
|
||||
})
|
||||
invoice_line = self.env['account.invoice.line'].create({
|
||||
'product_id': self.env.ref('product.product_product_4').id,
|
||||
'quantity': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'invoice_id': invoice.id,
|
||||
'name': 'product that cost 100',
|
||||
'account_id': invoice_line_account,
|
||||
})
|
||||
invoice_line2 = invoice_line.copy()
|
||||
|
||||
self.assertFalse(invoice_line.spread_id)
|
||||
self.assertEqual(invoice_line.spread_check, 'unlinked')
|
||||
|
||||
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,
|
||||
})
|
||||
invoice_line.spread_id = spread
|
||||
self.assertTrue(invoice_line.spread_id)
|
||||
self.assertEqual(invoice_line.spread_check, 'linked')
|
||||
self.assertFalse(invoice_line2.spread_id)
|
||||
self.assertEqual(invoice_line2.spread_check, 'unlinked')
|
||||
|
||||
invoice.action_invoice_open()
|
||||
self.assertTrue(invoice_line.spread_id)
|
||||
self.assertEqual(invoice_line.spread_check, 'linked')
|
||||
self.assertFalse(invoice_line2.spread_id)
|
||||
self.assertEqual(invoice_line2.spread_check, 'unavailable')
|
||||
|
||||
def test_07_create_spread_template(self):
|
||||
account_revenue = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_revenue').id)],
|
||||
limit=1)
|
||||
account_payable = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_payable').id)],
|
||||
limit=1)
|
||||
spread_template = self.env['account.spread.template'].create({
|
||||
'name': 'test',
|
||||
'spread_type': 'sale',
|
||||
'spread_account_id': account_revenue.id,
|
||||
})
|
||||
|
||||
my_company = self.env.user.company_id
|
||||
self.assertEqual(spread_template.company_id, my_company)
|
||||
self.assertTrue(spread_template.spread_journal_id)
|
||||
|
||||
exp_journal = self.ref('account_spread_cost_revenue.expenses_journal')
|
||||
sales_journal = self.ref('account_spread_cost_revenue.sales_journal')
|
||||
my_company.default_spread_revenue_account_id = account_revenue
|
||||
my_company.default_spread_expense_account_id = account_payable
|
||||
my_company.default_spread_revenue_journal_id = sales_journal
|
||||
my_company.default_spread_expense_journal_id = exp_journal
|
||||
|
||||
spread_template.spread_type = 'purchase'
|
||||
spread_template.onchange_spread_type()
|
||||
self.assertTrue(spread_template.spread_journal_id)
|
||||
self.assertTrue(spread_template.spread_account_id)
|
||||
self.assertEqual(spread_template.spread_account_id, account_payable)
|
||||
self.assertEqual(spread_template.spread_journal_id.id, exp_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'
|
||||
spread_template.onchange_spread_type()
|
||||
self.assertTrue(spread_template.spread_journal_id)
|
||||
self.assertTrue(spread_template.spread_account_id)
|
||||
self.assertEqual(spread_template.spread_account_id, account_revenue)
|
||||
self.assertEqual(spread_template.spread_journal_id.id, 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):
|
||||
account_revenue = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_revenue').id)],
|
||||
limit=1)
|
||||
account_payable = self.env['account.account'].search([(
|
||||
'user_type_id',
|
||||
'=',
|
||||
self.env.ref('account.data_account_type_payable').id)],
|
||||
limit=1)
|
||||
template_sale = self.env['account.spread.template'].create({
|
||||
'name': 'test',
|
||||
'spread_type': 'sale',
|
||||
'spread_account_id': account_revenue.id,
|
||||
})
|
||||
template_purchase = self.env['account.spread.template'].create({
|
||||
'name': 'test',
|
||||
'spread_type': 'purchase',
|
||||
'spread_account_id': 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,
|
||||
})
|
||||
spread.template_id = template_sale
|
||||
self.assertEqual(spread.template_id, template_sale)
|
||||
with self.assertRaises(ValidationError):
|
||||
spread.template_id = template_purchase
|
||||
self.assertEqual(spread.invoice_type, 'out_invoice')
|
||||
spread.onchange_template()
|
||||
self.assertEqual(spread.invoice_type, 'in_invoice')
|
||||
|
||||
spread.template_id = False
|
||||
spread.invoice_type = 'in_invoice'
|
||||
spread.template_id = template_purchase
|
||||
self.assertEqual(spread.template_id, template_purchase)
|
||||
with self.assertRaises(ValidationError):
|
||||
spread.template_id = template_sale
|
||||
self.assertEqual(spread.invoice_type, 'in_invoice')
|
||||
spread.onchange_template()
|
||||
self.assertEqual(spread.invoice_type, 'out_invoice')
|
||||
|
||||
spread.template_id = False
|
||||
spread.invoice_type = 'out_invoice'
|
||||
spread.template_id = template_sale
|
||||
self.assertEqual(spread.template_id, template_sale)
|
||||
with self.assertRaises(ValidationError):
|
||||
spread.invoice_type = 'in_invoice'
|
||||
self.assertEqual(spread.invoice_type, 'in_invoice')
|
||||
spread.onchange_template()
|
||||
self.assertEqual(spread.invoice_type, 'out_invoice')
|
||||
|
||||
spread.template_id = False
|
||||
spread.invoice_type = 'in_invoice'
|
||||
spread.template_id = template_purchase
|
||||
self.assertEqual(spread.template_id, template_purchase)
|
||||
with self.assertRaises(ValidationError):
|
||||
spread.invoice_type = 'out_invoice'
|
||||
self.assertEqual(spread.invoice_type, 'out_invoice')
|
||||
spread.onchange_template()
|
||||
self.assertEqual(spread.invoice_type, 'in_invoice')
|
||||
|
||||
def test_09_wrong_invoice_type(self):
|
||||
invoice_account = self.env['account.account'].search([
|
||||
('user_type_id', '=', self.env.ref(
|
||||
'account.data_account_type_receivable').id)
|
||||
], limit=1).id
|
||||
invoice_line_account = self.env['account.account'].search([
|
||||
('user_type_id', '=', self.env.ref(
|
||||
'account.data_account_type_expenses').id)
|
||||
], limit=1).id
|
||||
invoice = self.env['account.invoice'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'account_id': invoice_account,
|
||||
'type': 'in_invoice',
|
||||
})
|
||||
invoice_line = self.env['account.invoice.line'].create({
|
||||
'product_id': self.env.ref('product.product_product_4').id,
|
||||
'quantity': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'invoice_id': invoice.id,
|
||||
'name': 'product that cost 100',
|
||||
'account_id': invoice_line_account,
|
||||
})
|
||||
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):
|
||||
invoice_line.spread_id = spread
|
597
account_spread_cost_revenue/tests/test_compute_spread_board.py
Normal file
597
account_spread_cost_revenue/tests/test_compute_spread_board.py
Normal file
@ -0,0 +1,597 @@
|
||||
# Copyright 2017-2018 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestComputeSpreadBoard(AccountingTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
type_receivable = self.env.ref('account.data_account_type_receivable')
|
||||
type_expenses = self.env.ref('account.data_account_type_expenses')
|
||||
|
||||
def get_account(obj):
|
||||
return self.env['account.account'].search([
|
||||
('user_type_id', '=', obj.id)
|
||||
], limit=1)
|
||||
|
||||
journal = self.env['account.journal'].search([
|
||||
('type', '=', 'general')],
|
||||
limit=1)
|
||||
|
||||
self.receivable_account = get_account(type_receivable)
|
||||
self.expense_account = get_account(type_expenses)
|
||||
|
||||
self.spread_account = self.env['account.account'].search([
|
||||
('user_type_id', '=', type_expenses.id),
|
||||
('id', '!=', self.expense_account.id)
|
||||
], limit=1)
|
||||
|
||||
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',
|
||||
})
|
||||
|
||||
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('2017-02-28', spread_lines[0].date)
|
||||
self.assertEqual('2017-03-31', spread_lines[1].date)
|
||||
self.assertEqual('2017-04-30', spread_lines[2].date)
|
||||
self.assertEqual('2017-05-31', spread_lines[3].date)
|
||||
self.assertEqual('2017-06-30', spread_lines[4].date)
|
||||
self.assertEqual('2017-07-31', spread_lines[5].date)
|
||||
self.assertEqual('2017-08-31', spread_lines[6].date)
|
||||
self.assertEqual('2017-09-30', spread_lines[7].date)
|
||||
self.assertEqual('2017-10-31', spread_lines[8].date)
|
||||
self.assertEqual('2017-11-30', spread_lines[9].date)
|
||||
self.assertEqual('2017-12-31', spread_lines[10].date)
|
||||
self.assertEqual('2018-01-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.journal_id.update_posted = True
|
||||
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': '2017-01-07'
|
||||
})
|
||||
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('2017-01-31', spread_lines[0].date)
|
||||
self.assertEqual('2017-02-28', spread_lines[1].date)
|
||||
self.assertEqual('2017-03-31', spread_lines[2].date)
|
||||
self.assertEqual('2017-04-30', spread_lines[3].date)
|
||||
self.assertEqual('2017-05-31', spread_lines[4].date)
|
||||
self.assertEqual('2017-06-30', spread_lines[5].date)
|
||||
self.assertEqual('2017-07-31', spread_lines[6].date)
|
||||
self.assertEqual('2017-08-31', spread_lines[7].date)
|
||||
self.assertEqual('2017-09-30', spread_lines[8].date)
|
||||
self.assertEqual('2017-10-31', spread_lines[9].date)
|
||||
self.assertEqual('2017-11-30', spread_lines[10].date)
|
||||
self.assertEqual('2017-12-31', spread_lines[11].date)
|
||||
self.assertEqual('2018-01-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': '2017-01-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('2017-01-31', spread_lines[0].date)
|
||||
self.assertEqual('2017-02-28', spread_lines[1].date)
|
||||
self.assertEqual('2017-03-31', spread_lines[2].date)
|
||||
self.assertEqual('2017-04-30', spread_lines[3].date)
|
||||
self.assertEqual('2017-05-31', spread_lines[4].date)
|
||||
self.assertEqual('2017-06-30', spread_lines[5].date)
|
||||
self.assertEqual('2017-07-31', spread_lines[6].date)
|
||||
self.assertEqual('2017-08-31', spread_lines[7].date)
|
||||
self.assertEqual('2017-09-30', spread_lines[8].date)
|
||||
self.assertEqual('2017-10-31', spread_lines[9].date)
|
||||
self.assertEqual('2017-11-30', spread_lines[10].date)
|
||||
self.assertEqual('2017-12-31', spread_lines[11].date)
|
||||
self.assertEqual('2018-01-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('2017-01-31', spread_lines[0].date)
|
||||
self.assertEqual('2017-02-28', spread_lines[1].date)
|
||||
self.assertEqual('2017-03-31', spread_lines[2].date)
|
||||
self.assertEqual('2017-04-30', spread_lines[3].date)
|
||||
self.assertEqual('2017-05-31', spread_lines[4].date)
|
||||
self.assertEqual('2017-06-30', spread_lines[5].date)
|
||||
self.assertEqual('2017-07-31', spread_lines[6].date)
|
||||
self.assertEqual('2017-08-31', spread_lines[7].date)
|
||||
self.assertEqual('2017-09-30', spread_lines[8].date)
|
||||
self.assertEqual('2017-10-31', spread_lines[9].date)
|
||||
self.assertEqual('2017-11-30', spread_lines[10].date)
|
||||
self.assertEqual('2017-12-31', spread_lines[11].date)
|
||||
self.assertEqual('2018-01-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': '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': '2017-02-01'
|
||||
})
|
||||
|
||||
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))
|
||||
|
||||
# post and then unlink all created moves
|
||||
self.spread.journal_id.write({'update_posted': True})
|
||||
for line in self.spread.line_ids:
|
||||
line.move_id.post()
|
||||
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.)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.)
|
||||
|
||||
# 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.)
|
||||
self.assertEqual(self.spread.unposted_amount, 0.)
|
||||
|
||||
def test_07_supplier_invoice(self):
|
||||
self.spread.write({
|
||||
'period_number': 3,
|
||||
'period_type': 'month',
|
||||
'spread_date': '2017-01-01',
|
||||
'estimated_amount': 345.96,
|
||||
})
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 3)
|
||||
self.assertEqual(115.32, spread_lines[0].amount)
|
||||
self.assertEqual(115.32, spread_lines[1].amount)
|
||||
self.assertEqual(115.32, spread_lines[2].amount)
|
||||
self.assertEqual('2017-01-31', spread_lines[0].date)
|
||||
self.assertEqual('2017-02-28', spread_lines[1].date)
|
||||
self.assertEqual('2017-03-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': '2017-02-01'
|
||||
})
|
||||
|
||||
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': '2017-02-01'
|
||||
})
|
||||
|
||||
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.journal_id.update_posted = True
|
||||
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.)
|
||||
self.assertEqual(self.spread.unposted_amount, 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': '2017-01-07'
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.spread.compute_spread_board()
|
||||
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertFalse(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': '2017-01-07'
|
||||
})
|
||||
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().post()
|
||||
spread_lines[1]._create_moves().post()
|
||||
spread_lines[2]._create_moves().post()
|
||||
spread_lines[3]._create_moves().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('2017-01-31', spread_lines[0].date)
|
||||
self.assertEqual('2017-02-28', spread_lines[1].date)
|
||||
self.assertEqual('2017-03-31', spread_lines[2].date)
|
||||
self.assertEqual('2017-04-30', spread_lines[3].date)
|
||||
self.assertEqual('2017-05-31', spread_lines[4].date)
|
||||
self.assertEqual('2017-06-30', spread_lines[5].date)
|
||||
self.assertEqual('2017-07-31', spread_lines[6].date)
|
||||
self.assertEqual('2017-08-31', spread_lines[7].date)
|
||||
self.assertEqual('2017-09-30', spread_lines[8].date)
|
||||
self.assertEqual('2017-10-31', spread_lines[9].date)
|
||||
self.assertEqual('2017-11-30', spread_lines[10].date)
|
||||
self.assertEqual('2017-12-31', spread_lines[11].date)
|
||||
self.assertEqual('2018-01-31', spread_lines[12].date)
|
||||
|
||||
self.assertAlmostEqual(self.spread.unspread_amount, 682.81)
|
||||
self.assertAlmostEqual(self.spread.unposted_amount, 682.81)
|
24
account_spread_cost_revenue/views/account_invoice.xml
Normal file
24
account_spread_cost_revenue/views/account_invoice.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_supplier_invoice_spread" model="ir.ui.view">
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="inherit_id" ref="account.invoice_supplier_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']" position="before">
|
||||
<field name="spread_check" widget="spread_line_widget" groups="account.group_account_user,account.group_account_manager"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_customer_invoice_spread" model="ir.ui.view">
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="inherit_id" ref="account.invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']" position="before">
|
||||
<field name="spread_check" widget="spread_line_widget" groups="account.group_account_user,account.group_account_manager"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
178
account_spread_cost_revenue/views/account_spread.xml
Normal file
178
account_spread_cost_revenue/views/account_spread.xml
Normal file
@ -0,0 +1,178 @@
|
||||
<?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)]}" />
|
||||
<button name="action_recalculate_spread" type="object" string="Recalculate entire spread" attrs="{'invisible': [('debit_account_id', '=', 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 class="oe_button_box">
|
||||
<button name="open_reconcile_view"
|
||||
class="oe_stat_button"
|
||||
icon="fa-bars"
|
||||
type="object"
|
||||
string="Reconciled entries">
|
||||
</button>
|
||||
</div>
|
||||
<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)]}"/>
|
||||
</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'))]}"/>
|
||||
<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>
|
||||
<field name="debit_account_id" required="1" domain="[('company_id', '=', company_id), ('deprecated', '=', False)]" 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'))]}"/>
|
||||
<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>
|
||||
<field name="credit_account_id" required="1" domain="[('company_id', '=', company_id), ('deprecated', '=', False)]" 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" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="period_number"/>
|
||||
<field name="period_type"/>
|
||||
<field name="spread_date"/>
|
||||
<field name="journal_id" domain="[('company_id', '=', company_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 fa-play" string="Create Move" type="object" groups="account.group_account_manager"
|
||||
attrs="{'invisible':['|',('move_id','!=',False)]}"/>
|
||||
<button name="open_move" icon="fa fa-plus-square-o" string="View Move" type="object"
|
||||
attrs="{'invisible':[('move_id','=',False)]}"/>
|
||||
<button name="unlink_move" icon="fa 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="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="account_analytic_id" domain="[('company_id', '=', company_id)]" groups="analytic.group_analytic_accounting" options="{'no_create': True}"/>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags"/>
|
||||
</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_account_manager" model="ir.ui.view">
|
||||
<field name="model">account.spread</field>
|
||||
<field name="inherit_id" ref="view_account_spread" />
|
||||
<field name="groups_id" eval="[(4, ref('account.group_account_manager'))]" />
|
||||
<field name="arch" type="xml">
|
||||
<tree position="attributes">
|
||||
<attribute name="editable">1</attribute>
|
||||
</tree>
|
||||
<field name="amount" position="attributes">
|
||||
<attribute name="readonly"/>
|
||||
</field>
|
||||
<field name="line_ids" position="attributes">
|
||||
<attribute name="readonly"/>
|
||||
</field>
|
||||
</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="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_type">form</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_entries"
|
||||
action="action_account_spread_form"
|
||||
groups="account.group_account_user,account.group_account_manager"/>
|
||||
|
||||
</odoo>
|
@ -0,0 +1,55 @@
|
||||
<?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"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="spread_account_id" domain="[('company_id', '=', company_id), ('deprecated', '=', False)]" options="{'no_create': True}"/>
|
||||
<field name="spread_journal_id" domain="[('company_id', '=', company_id)]" widget="selection"/>
|
||||
</group>
|
||||
</group>
|
||||
</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_type">form</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>
|
25
account_spread_cost_revenue/views/res_company.xml
Normal file
25
account_spread_cost_revenue/views/res_company.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?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">
|
||||
<field name="default_spread_revenue_account_id" />
|
||||
<field name="default_spread_expense_account_id" />
|
||||
</group>
|
||||
<group string="Default Spread Journals">
|
||||
<field name="default_spread_revenue_journal_id" />
|
||||
<field name="default_spread_expense_journal_id" />
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
3
account_spread_cost_revenue/wizards/__init__.py
Normal file
3
account_spread_cost_revenue/wizards/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account_spread_invoice_line_link_wizard
|
@ -0,0 +1,181 @@
|
||||
# Copyright 2018 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'
|
||||
|
||||
invoice_line_id = fields.Many2one(
|
||||
'account.invoice.line',
|
||||
string='Invoice Line',
|
||||
readonly=True,
|
||||
required=True,
|
||||
ondelete='cascade')
|
||||
invoice_id = fields.Many2one(
|
||||
related='invoice_line_id.invoice_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_id = fields.Many2one(
|
||||
'account.spread',
|
||||
string='Spread Board')
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True)
|
||||
spread_action_type = fields.Selection([
|
||||
('link', 'Link to existing spread board'),
|
||||
('template', 'Create from spread template'),
|
||||
('new', 'Create new spread board')],
|
||||
default='link')
|
||||
template_id = fields.Many2one(
|
||||
'account.spread.template',
|
||||
string='Spread Template')
|
||||
spread_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='Balance sheet account / Spread account',
|
||||
store=True)
|
||||
spread_journal_id = fields.Many2one(
|
||||
'account.journal',
|
||||
string='Spread Journal',
|
||||
store=True)
|
||||
|
||||
@api.depends('invoice_line_id')
|
||||
def _compute_invoice_type(self):
|
||||
for wizard in self:
|
||||
invoice = wizard.invoice_line_id.invoice_id
|
||||
wizard.invoice_type = invoice.type
|
||||
if invoice.type in ['out_invoice', 'out_refund']:
|
||||
wizard.spread_type = 'sale'
|
||||
else:
|
||||
wizard.spread_type = 'purchase'
|
||||
|
||||
@api.onchange('company_id', 'invoice_type')
|
||||
def _onchange_spread_journal_account(self):
|
||||
for wizard in self:
|
||||
company = wizard.company_id
|
||||
acc_revenue = company.default_spread_revenue_account_id
|
||||
acc_expense = company.default_spread_expense_account_id
|
||||
journal_revenue = company.default_spread_revenue_journal_id
|
||||
journal_expense = company.default_spread_expense_journal_id
|
||||
if wizard.invoice_type in ('out_invoice', 'in_refund'):
|
||||
wizard.spread_account_id = acc_revenue
|
||||
wizard.spread_journal_id = journal_revenue
|
||||
else:
|
||||
wizard.spread_account_id = acc_expense
|
||||
wizard.spread_journal_id = journal_expense
|
||||
|
||||
@api.multi
|
||||
def _inverse_spread_journal_account(self):
|
||||
"""Keep this for making the fields editable"""
|
||||
pass
|
||||
|
||||
@api.onchange('invoice_type')
|
||||
def onchange_invoice_type(self):
|
||||
domain = [
|
||||
('invoice_id', '=', False),
|
||||
('invoice_type', '=', self.invoice_type),
|
||||
('company_id', '=', self.company_id.id)]
|
||||
res = {'domain': {'spread_id': domain}}
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
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_type': 'form',
|
||||
'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.invoice_line_id.account_id
|
||||
else:
|
||||
debit_account = self.invoice_line_id.account_id
|
||||
|
||||
analytic_account = self.invoice_line_id.account_analytic_id
|
||||
analytic_tags = self.invoice_line_id.analytic_tag_ids
|
||||
date_invoice = self.invoice_id.date_invoice 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_debit_account_id': debit_account.id,
|
||||
'default_credit_account_id': credit_account.id,
|
||||
'default_journal_id': self.spread_journal_id.id,
|
||||
'default_account_analytic_id': analytic_account.id,
|
||||
'default_analytic_tag_ids': analytic_tags.ids,
|
||||
'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_vals = self.template_id._prepare_spread_from_template()
|
||||
|
||||
date_invoice = self.invoice_id.date_invoice
|
||||
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_account = self.invoice_line_id.account_analytic_id
|
||||
spread_vals['account_analytic_id'] = analytic_account.id
|
||||
|
||||
spread = self.env['account.spread'].create(spread_vals)
|
||||
|
||||
analytic_tags = self.invoice_line_id.analytic_tag_ids
|
||||
spread.analytic_tag_ids = analytic_tags
|
||||
|
||||
self.invoice_line_id.spread_id = spread
|
||||
return {
|
||||
'name': _('Spread Details'),
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'account.spread',
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'current',
|
||||
'readonly': False,
|
||||
'res_id': self.invoice_line_id.spread_id.id,
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?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="spread_account_id" attrs="{'invisible': [('spread_action_type', '!=', 'new')],'required': [('spread_action_type', '=', 'new')]}" domain="[('company_id', '=', company_id), ('deprecated', '=', False)]" options="{'no_create': True}"/>
|
||||
<field name="spread_journal_id" attrs="{'invisible': [('spread_action_type', '!=', 'new')],'required': [('spread_action_type', '=', 'new')]}" domain="[('company_id', '=', company_id)]" 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>
|
Loading…
Reference in New Issue
Block a user