2
0

[ADD] account_spread_cost_revenue

This commit is contained in:
Andrea 2018-10-22 12:27:50 +02:00 committed by Andrea Stirpe
parent 75fc69fc2c
commit f0e84cebd9
41 changed files with 3923 additions and 0 deletions

View 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.

View File

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

View File

@ -0,0 +1,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,
}

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

View 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

View 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

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

View 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()

View 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()

View File

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

View 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')

View 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*.

View File

@ -0,0 +1 @@
* Andrea Stirpe <a.stirpe@onestein.nl>

View File

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

View File

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

View File

@ -0,0 +1,5 @@
11.0.1.0.0
~~~~~~~~~~
* [ADD] Module account_spread_cost_revenue.
(`#715 <https://github.com/OCA/account-financial-tools/pull/715>`_)

View File

@ -0,0 +1,2 @@
* Verify last day of month
* Add help in fields definition

View 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.

View File

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

View 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_spread_cost_revenue_full Full access on account.spread model_account_spread account.group_account_manager 1 1 1 1
3 access_account_spread_cost_revenue_read Read access on account.spread model_account_spread account.group_account_user 1 0 0 0
4 access_account_spread_cost_revenue_line_full Full access on account.spread.line model_account_spread_line account.group_account_manager 1 1 1 1
5 access_account_spread_cost_revenue_line_read Read access on account.spread.line model_account_spread_line account.group_account_user 1 0 0 0
6 access_account_spread_cost_revenue_template_full Full access on account.spread.template model_account_spread_template account.group_account_manager 1 1 1 1
7 access_account_spread_cost_revenue_template_read Read access on account.spread.template model_account_spread_template account.group_account_user 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,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 -&gt; Adviser -&gt; Accounting Entries -&gt; Spread Costs/Revenues, create a new spread board.</p>
<p>Complete the definition of the spreading criteria, by setting the the fields:</p>
<ul class="simple">
<li><em>Debit Account</em></li>
<li><em>Credit Account</em></li>
<li><em>Estimated Amount</em> (The total amount to spread)</li>
<li><em>Number of Repetitions</em></li>
<li><em>Period Type</em> (Duration of each period)</li>
<li><em>Start date</em></li>
<li><em>Journal</em></li>
</ul>
<div class="figure">
<img alt="Create a new spread board" src="https://raw.githubusercontent.com/OCA/account-financial-tools/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 &lt;<a class="reference external" href="mailto:a.stirpe&#64;onestein.nl">a.stirpe&#64;onestein.nl</a>&gt;</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

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,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);
});

View File

@ -0,0 +1,5 @@
.o_web_client .o_spread_line {
&.o_is_linked {
color: @brand-success;
}
}

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

View 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

View 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

View File

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

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

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

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

View File

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

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

View File

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

View File

@ -0,0 +1,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,
}

View File

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