Files
tryton/modules/account/move.py
T

3159 lines
114 KiB
Python
Raw Normal View History

2014-12-02 12:54:29 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2014-05-09 23:32:17 +02:00
from collections import defaultdict
2021-12-11 17:58:41 +01:00
from decimal import Decimal
from itertools import chain, combinations, groupby, islice
2014-05-09 23:32:17 +02:00
2021-08-20 23:45:02 +02:00
from dateutil.relativedelta import relativedelta
from sql import Literal, Null, Window
2023-01-19 11:25:25 +01:00
from sql.aggregate import Sum
2021-12-11 17:58:41 +01:00
from sql.conditionals import Case, Coalesce
from sql.functions import Abs, CharLength, Round
from sql.operators import Exists
2014-09-19 16:24:48 +02:00
from trytond.config import config
2021-12-11 17:58:41 +01:00
from trytond.i18n import gettext
from trytond.model import (
Check, DeactivableMixin, Index, ModelSQL, ModelView, dualmethod, fields)
2021-12-11 17:58:41 +01:00
from trytond.model.exceptions import AccessError
2021-08-21 09:09:55 +02:00
from trytond.modules.currency.fields import Monetary
2021-12-11 17:58:41 +01:00
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, If, PYSONEncoder
from trytond.report import Report
from trytond.rpc import RPC
from trytond.tools import firstline, grouped_slice, reduce_ids
from trytond.transaction import Transaction, check_access
2021-12-11 17:58:41 +01:00
from trytond.wizard import (
Button, StateAction, StateTransition, StateView, Wizard)
2021-08-21 09:09:55 +02:00
2021-12-11 17:58:41 +01:00
from .exceptions import (
CancelDelegatedWarning, CancelWarning, DelegateLineError, GroupLineError,
PeriodNotFoundError, PostError, ReconciliationDeleteWarning,
ReconciliationError, RescheduleLineError)
2018-12-29 14:20:28 +01:00
2008-03-17 18:32:06 +01:00
_MOVE_STATES = {
2011-08-21 11:20:29 +02:00
'readonly': Eval('state') == 'posted',
}
2008-03-17 18:32:06 +01:00
_LINE_STATES = {
2011-08-21 11:20:29 +02:00
'readonly': Eval('state') == 'valid',
}
2008-03-17 18:32:06 +01:00
class DescriptionOriginMixin:
__slots__ = ()
description_used = fields.Function(
fields.Char("Description", states=_MOVE_STATES),
'on_change_with_description_used',
setter='set_description_used',
searcher='search_description_used')
@fields.depends('description', 'origin')
def on_change_with_description_used(self, name=None):
description = self.description
if not description and hasattr(self.origin, 'description'):
description = firstline(self.origin.description or '')
return description
@classmethod
def set_description_used(cls, moves, name, value):
moves = [m for m in moves if m.description_used != value]
if moves:
cls.write(moves, {'description': value})
@classmethod
def search_description_used(cls, name, clause):
pool = Pool()
domain = ['OR', [
('description', '!=', None),
('description', '!=', ''),
('description', *clause[1:]),
],
]
for origin, _ in cls.get_origin():
if not origin:
continue
Model = pool.get(origin)
if 'description' in Model._fields:
domain.append(
('origin.description', *clause[1:3], origin, *clause[3:]))
return domain
class Move(DescriptionOriginMixin, ModelSQL, ModelView):
2008-03-17 18:32:06 +01:00
'Account Move'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move'
2012-09-29 15:33:21 +02:00
_rec_name = 'number'
number = fields.Char('Number', required=True, readonly=True)
post_number = fields.Char('Post Number', readonly=True,
2019-12-18 09:16:34 +01:00
help='Also known as Folio Number.')
company = fields.Many2One('company.company', 'Company', required=True,
states=_MOVE_STATES)
2008-03-17 18:32:06 +01:00
period = fields.Many2One('account.period', 'Period', required=True,
2014-12-03 16:13:08 +01:00
domain=[
('company', '=', Eval('company', -1)),
If(Eval('state') == 'draft',
('state', '=', 'open'),
()),
2023-02-14 01:32:04 +01:00
If(Eval('date'), [
('start_date', '<=', Eval('date')),
('end_date', '>=', Eval('date')),
],
[]),
2014-12-03 16:13:08 +01:00
],
states=_MOVE_STATES)
2008-03-17 18:32:06 +01:00
journal = fields.Many2One('account.journal', 'Journal', required=True,
2020-12-17 22:21:43 +01:00
states={
'readonly': Eval('number') & Eval('journal'),
},
context={
'company': Eval('company', -1),
},
depends={'company'})
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES)
2008-03-28 19:49:16 +01:00
post_date = fields.Date('Post Date', readonly=True)
description = fields.Char('Description', states=_MOVE_STATES)
2012-09-29 15:33:21 +02:00
origin = fields.Reference('Origin', selection='get_origin',
states=_MOVE_STATES)
2008-03-17 18:32:06 +01:00
state = fields.Selection([
('draft', 'Draft'),
('posted', 'Posted'),
], 'State', required=True, readonly=True, sort=False)
2008-03-17 18:32:06 +01:00
lines = fields.One2Many('account.move.line', 'move', 'Lines',
states=_MOVE_STATES, depends={'company'},
2020-03-01 12:33:50 +01:00
context={
'journal': Eval('journal'),
'period': Eval('period'),
'date': Eval('date'),
2010-01-20 10:56:33 +01:00
})
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def __setup__(cls):
cls.number.search_unaccented = False
cls.post_number.search_unaccented = False
2012-09-24 22:17:29 +02:00
super(Move, cls).__setup__()
t = cls.__table__()
2022-02-03 22:29:08 +01:00
cls.create_date.select = True
2020-06-17 13:58:44 +02:00
cls._check_modify_exclude = ['lines']
2012-09-24 22:17:29 +02:00
cls._order.insert(0, ('date', 'DESC'))
2012-09-29 15:33:21 +02:00
cls._order.insert(1, ('number', 'DESC'))
2012-09-24 22:17:29 +02:00
cls._buttons.update({
2012-03-25 00:17:23 +01:00
'post': {
'invisible': Eval('state') == 'posted',
2018-02-19 11:36:13 +01:00
'depends': ['state'],
2012-03-25 00:17:23 +01:00
},
})
2018-08-26 18:06:32 +02:00
cls.__rpc__.update({
'post': RPC(
readonly=False, instantiate=0, fresh_session=True),
})
cls._sql_indexes.update({
Index(t, (t.period, Index.Equality())),
Index(t, (t.date, Index.Range()), (t.number, Index.Range())),
Index(
t,
(t.journal, Index.Equality()),
(t.period, Index.Equality())),
})
2008-03-17 18:32:06 +01:00
@classmethod
def order_number(cls, tables):
table, _ = tables[None]
return [CharLength(table.number), table.number]
@classmethod
def order_post_number(cls, tables):
table, _ = tables[None]
return [CharLength(table.post_number), table.post_number]
2014-12-03 16:13:08 +01:00
@staticmethod
def default_company():
return Transaction().context.get('company')
@classmethod
def default_period(cls):
2012-09-24 22:17:29 +02:00
Period = Pool().get('account.period')
company = cls.default_company()
try:
period = Period.find(company)
except PeriodNotFoundError:
return None
return period.id
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@staticmethod
def default_state():
2008-03-17 18:32:06 +01:00
return 'draft'
2012-09-24 22:17:29 +02:00
@classmethod
def default_date(cls):
pool = Pool()
Period = pool.get('account.period')
Date = pool.get('ir.date')
today = Date.today()
2012-09-24 22:17:29 +02:00
period_id = cls.default_period()
2009-10-10 13:31:39 +02:00
if period_id:
2012-09-24 22:17:29 +02:00
period = Period(period_id)
2023-02-14 01:32:04 +01:00
start_date = period.start_date
end_date = period.end_date
if start_date <= today <= end_date:
return today
elif start_date >= today:
return start_date
else:
return end_date
2009-10-10 13:31:39 +02:00
2023-02-14 01:32:04 +01:00
@fields.depends('date', 'period', 'company', 'lines')
def on_change_date(self):
2023-02-14 01:32:04 +01:00
pool = Pool()
Period = pool.get('account.period')
context = Transaction().context
if self.date:
company = self.company or context.get('company')
if (not self.period
or not (
self.period.start_date <= self.date
<= self.period.end_date)):
try:
self.period = Period.find(company, date=self.date)
except PeriodNotFoundError:
self.period = None
for line in (self.lines or []):
line.date = self.date
2023-02-14 01:32:04 +01:00
@fields.depends(
'period', 'date', 'company', 'journal', methods=['on_change_date'])
def on_change_period(self):
2023-02-14 01:32:04 +01:00
pool = Pool()
Date = pool.get('ir.date')
Line = pool.get('account.move.line')
context = Transaction().context
company_id = (
self.company.id if self.company else context.get('company'))
with Transaction().set_context(company=company_id):
today = Date.today()
if self.period:
start_date = self.period.start_date
end_date = self.period.end_date
if (not self.date
or not (start_date <= self.date <= end_date)):
if start_date <= today <= end_date:
self.date = today
else:
lines = Line.search([
('company', '=', company_id),
('journal', '=', self.journal),
('period', '=', self.period),
],
order=[('date', 'DESC')], limit=1)
if lines:
line, = lines
self.date = line.date
elif start_date >= today:
self.date = start_date
else:
self.date = end_date
self.on_change_date()
2012-09-29 15:33:21 +02:00
@classmethod
def _get_origin(cls):
'Return list of Model names for origin Reference'
2014-05-09 23:32:17 +02:00
return ['account.fiscalyear', 'account.move']
2012-09-29 15:33:21 +02:00
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
2020-12-27 00:19:30 +01:00
get_name = Model.get_name
2012-09-29 15:33:21 +02:00
models = cls._get_origin()
2020-12-27 00:19:30 +01:00
return [(None, '')] + [(m, get_name(m)) for m in models]
2012-09-29 15:33:21 +02:00
2012-09-24 22:17:29 +02:00
@classmethod
def check_modify(cls, moves):
2012-03-02 07:46:19 +00:00
'Check posted moves for modifications.'
2012-09-24 22:17:29 +02:00
for move in moves:
2012-03-02 07:46:19 +00:00
if move.state == 'posted':
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account.msg_modify_posted_moved',
move=move.rec_name))
2012-03-02 07:46:19 +00:00
2012-09-24 22:17:29 +02:00
@classmethod
def search_rec_name(cls, name, clause):
2015-04-05 23:39:57 +02:00
if clause[1].startswith('!') or clause[1].startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
2014-01-31 14:06:56 +01:00
('post_number',) + tuple(clause[1:]),
(cls._rec_name,) + tuple(clause[1:]),
]
2012-09-24 22:17:29 +02:00
@classmethod
2013-12-19 14:44:33 +01:00
def write(cls, *args):
actions = iter(args)
args = []
for moves, values in zip(actions, actions):
2018-06-26 19:11:40 +02:00
keys = list(values.keys())
2013-12-19 14:44:33 +01:00
for key in cls._check_modify_exclude:
if key in keys:
keys.remove(key)
if len(keys):
cls.check_modify(moves)
args.extend((moves, values))
super(Move, cls).write(*args)
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
2013-01-06 22:18:13 +01:00
def create(cls, vlist):
2011-07-03 13:18:40 +02:00
pool = Pool()
2012-09-24 22:17:29 +02:00
Journal = pool.get('account.journal')
context = Transaction().context
journals = {}
default_company = cls.default_company()
2013-01-06 22:18:13 +01:00
vlist = [x.copy() for x in vlist]
for vals in vlist:
if not vals.get('number'):
journal_id = vals.get('journal', context.get('journal'))
company_id = vals.get('company', default_company)
2013-01-06 22:18:13 +01:00
if journal_id:
if journal_id not in journals:
journal = journals[journal_id] = Journal(journal_id)
else:
journal = journals[journal_id]
sequence = journal.get_multivalue(
'sequence', company=company_id)
if sequence:
with Transaction().set_context(company=company_id):
vals['number'] = sequence.get()
return super().create(vlist)
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def delete(cls, moves):
MoveLine = Pool().get('account.move.line')
cls.check_modify(moves)
MoveLine.delete([l for m in moves for l in m.lines])
super(Move, cls).delete(moves)
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def copy(cls, moves, default=None):
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
default.setdefault('number', None)
default.setdefault('post_number', None)
default.setdefault('state', cls.default_state())
default.setdefault('post_date', None)
return super().copy(moves, default=default)
2008-12-05 13:25:33 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def validate_move(cls, moves):
2008-03-17 18:32:06 +01:00
'''
2013-05-21 19:09:37 +02:00
Validate balanced move
2008-03-17 18:32:06 +01:00
'''
2012-09-24 22:17:29 +02:00
pool = Pool()
MoveLine = pool.get('account.move.line')
2013-09-06 22:33:47 +02:00
line = MoveLine.__table__()
transaction = Transaction()
cursor = transaction.connection.cursor()
2013-02-23 14:09:33 +01:00
for company, moves in groupby(moves, key=lambda m: m.company):
currency = company.currency
for sub_moves in grouped_slice(list(moves)):
red_sql = reduce_ids(line.move, [m.id for m in sub_moves])
2013-02-23 14:09:33 +01:00
valid_move_query = line.select(
line.move,
2013-09-06 22:33:47 +02:00
where=red_sql,
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) < abs(currency.rounding))
cursor.execute(*line.update(
[line.state],
['valid'],
where=line.move.in_(valid_move_query)))
2013-02-23 14:09:33 +01:00
draft_move_query = line.select(
line.move,
where=red_sql,
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) >= abs(currency.rounding))
cursor.execute(*line.update(
[line.state],
['draft'],
where=line.move.in_(draft_move_query)))
2013-02-23 14:09:33 +01:00
Transaction().counter += 1
for cache in Transaction().cache.values():
if MoveLine.__name__ in cache:
cache_cls = cache[MoveLine.__name__]
cache_cls.clear()
2008-03-17 18:32:06 +01:00
2023-01-27 01:06:15 +01:00
def _cancel_default(self, reversal=False):
2014-05-09 23:32:17 +02:00
'Return default dictionary to cancel move'
pool = Pool()
Date = pool.get('ir.date')
Period = pool.get('account.period')
2018-12-29 14:20:28 +01:00
Warning = pool.get('res.user.warning')
2014-05-09 23:32:17 +02:00
default = {
'origin': str(self),
}
2023-06-20 10:13:56 +02:00
if self.period.state == 'closed':
2018-12-29 14:20:28 +01:00
key = '%s.cancel' % self
if Warning.check(key):
raise CancelWarning(key,
gettext('account.msg_move_cancel_closed_period',
2018-12-29 14:20:28 +01:00
move=self.rec_name))
with Transaction().set_context(company=self.company.id):
date = Date.today()
period = Period.find(self.company, date=date)
2014-05-09 23:32:17 +02:00
default.update({
'date': date,
'period': period.id,
2014-05-09 23:32:17 +02:00
})
2023-01-27 01:06:15 +01:00
if reversal:
default['lines.debit'] = lambda data: data['credit']
default['lines.credit'] = lambda data: data['debit']
else:
default['lines.debit'] = lambda data: data['debit'] * -1
default['lines.credit'] = lambda data: data['credit'] * -1
2018-09-01 01:02:03 +02:00
default['lines.amount_second_currency'] = (
lambda data: data['amount_second_currency'] * -1
if data['amount_second_currency']
else data['amount_second_currency'])
default['lines.tax_lines.amount'] = lambda data: data['amount'] * -1
default['lines.origin'] = (
lambda data: 'account.move.line,%s' % data['id'])
2014-05-09 23:32:17 +02:00
return default
2023-01-27 01:06:15 +01:00
def cancel(self, default=None, reversal=False):
2014-05-09 23:32:17 +02:00
'Return a cancel move'
2014-11-10 23:35:54 +01:00
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
2023-01-27 01:06:15 +01:00
default.update(self._cancel_default(reversal=reversal))
cancel_move, = self.copy([self], default=default)
2014-05-09 23:32:17 +02:00
return cancel_move
@dualmethod
2012-03-25 00:17:23 +01:00
@ModelView.button
2012-09-24 22:17:29 +02:00
def post(cls, moves):
2011-07-03 13:18:40 +02:00
pool = Pool()
2012-09-24 22:17:29 +02:00
Date = pool.get('ir.date')
Line = pool.get('account.move.line')
move = cls.__table__()
line = Line.__table__()
cursor = Transaction().connection.cursor()
to_reconcile = []
for company, c_moves in groupby(moves, lambda m: m.company):
currency = company.currency
for sub_moves in grouped_slice(list(c_moves)):
sub_moves_ids = [m.id for m in sub_moves]
cursor.execute(*move.select(
move.id,
where=reduce_ids(move.id, sub_moves_ids)
& ~Exists(line.select(
line.move,
where=line.move == move.id))))
try:
move_id, = cursor.fetchone()
except TypeError:
pass
else:
raise PostError(
gettext('account.msg_post_empty_move',
move=cls(move_id).rec_name))
cursor.execute(*line.select(
line.move,
where=reduce_ids(line.move, sub_moves_ids),
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) >= abs(currency.rounding)))
try:
move_id, = cursor.fetchone()
except TypeError:
pass
else:
raise PostError(
gettext('account.msg_post_unbalanced_move',
move=cls(move_id).rec_name))
cursor.execute(*line.select(
line.id,
where=reduce_ids(line.move, sub_moves_ids)
& (line.debit == Decimal(0))
& (line.credit == Decimal(0))
& ((line.amount_second_currency == Null)
| (line.amount_second_currency == Decimal(0)))
))
to_reconcile.extend(l for l, in cursor)
2008-04-10 11:22:16 +02:00
2008-03-17 18:32:06 +01:00
for move in moves:
2016-06-17 13:40:45 +02:00
move.state = 'posted'
if not move.post_number:
with Transaction().set_context(company=move.company.id):
move.post_date = Date.today()
2021-03-11 23:00:09 +01:00
move.post_number = move.period.post_move_sequence_used.get()
2008-03-17 18:32:06 +01:00
2023-01-29 01:08:22 +01:00
cls.save(moves)
def keyfunc(line):
# Set party last to avoid compare party instance and None
# party will always be None for the same account
return line.account, line.party
to_reconcile = Line.browse(sorted(
[l for l in Line.browse(to_reconcile) if l.account.reconcile],
key=keyfunc))
2023-01-23 17:05:55 +01:00
to_reconcile = [list(l) for _, l in groupby(to_reconcile, keyfunc)]
if to_reconcile:
Line.reconcile(*to_reconcile)
2008-03-17 18:32:06 +01:00
2009-02-28 16:57:10 +01:00
class Reconciliation(ModelSQL, ModelView):
2008-03-19 22:13:51 +01:00
'Account Move Reconciliation Lines'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.reconciliation'
2023-01-08 20:10:39 +01:00
_rec_name = 'number'
number = fields.Char("Number", required=True)
2021-03-21 22:38:05 +01:00
company = fields.Many2One('company.company', "Company", required=True)
lines = fields.One2Many(
'account.move.line', 'reconciliation', 'Lines',
domain=[
('move.company', '=', Eval('company', -1)),
])
date = fields.Date(
"Date", required=True,
2019-12-18 09:16:34 +01:00
help='Highest date of the reconciled lines.')
2019-04-12 19:43:53 +02:00
delegate_to = fields.Many2One(
'account.move.line', "Delegate To", ondelete="RESTRICT",
2021-03-21 22:38:05 +01:00
domain=[
('move.company', '=', Eval('company', -1)),
],
2019-04-12 19:43:53 +02:00
help="The line to which the reconciliation status is delegated.")
2008-03-19 22:13:51 +01:00
@classmethod
def __setup__(cls):
2023-01-08 20:10:39 +01:00
cls.number.search_unaccented = False
super().__setup__()
t = cls.__table__()
cls._sql_indexes.add(Index(t, (t.date, Index.Range())))
2016-02-03 21:23:34 +01:00
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
2018-07-24 22:01:07 +02:00
table = cls.__table_handler__(module_name)
2016-02-03 21:23:34 +01:00
sql_table = cls.__table__()
pool = Pool()
Move = pool.get('account.move')
Line = pool.get('account.move.line')
move = Move.__table__()
line = Line.__table__()
2021-03-21 22:38:05 +01:00
line_h = Line.__table_handler__(module_name)
2016-02-03 21:23:34 +01:00
2021-03-21 22:38:05 +01:00
company_exist = table.column_exist('company')
2016-02-03 21:23:34 +01:00
2023-01-08 20:10:39 +01:00
# Migration from 6.6: rename name to number
if table.column_exist('name') and not table.column_exist('number'):
table.column_rename('name', 'number')
2016-02-03 21:23:34 +01:00
super(Reconciliation, cls).__register__(module_name)
2021-03-21 22:38:05 +01:00
# Migration from 5.8: add company field
if not company_exist and line_h.column_exist('reconciliation'):
value = (line
.join(move, condition=line.move == move.id)
.select(
move.company,
where=line.reconciliation == sql_table.id,
group_by=move.company))
cursor.execute(*sql_table.update([sql_table.company], [value]))
@classmethod
2023-01-08 20:10:39 +01:00
def order_number(cls, tables):
table, _ = tables[None]
2023-01-08 20:10:39 +01:00
return [CharLength(table.number), table.number]
2021-03-21 22:38:05 +01:00
@classmethod
def default_company(cls):
return Transaction().context.get('company')
2012-09-24 22:17:29 +02:00
@classmethod
2013-01-06 22:18:13 +01:00
def create(cls, vlist):
2021-03-11 23:00:09 +01:00
pool = Pool()
Configuration = pool.get('account.configuration')
configuration = Configuration(1)
2013-01-06 22:18:13 +01:00
vlist = [x.copy() for x in vlist]
2021-04-03 14:27:09 +02:00
default_company = cls.default_company()
2013-01-06 22:18:13 +01:00
for vals in vlist:
2023-01-08 20:10:39 +01:00
if 'number' not in vals:
vals['number'] = configuration.get_multivalue(
2021-04-03 14:27:09 +02:00
'reconciliation_sequence',
company=vals.get('company', default_company)).get()
2013-01-06 22:18:13 +01:00
return super(Reconciliation, cls).create(vlist)
2008-03-19 22:13:51 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
2013-12-19 14:44:33 +01:00
def write(cls, moves, values, *args):
2018-12-29 14:20:28 +01:00
raise AccessError(gettext('account.msg_write_reconciliation'))
@classmethod
def validate(cls, reconciliations):
super(Reconciliation, cls).validate(reconciliations)
cls.check_lines(reconciliations)
2019-04-12 19:43:53 +02:00
@classmethod
def delete(cls, reconciliations):
pool = Pool()
Warning = pool.get('res.user.warning')
for reconciliation in reconciliations:
if reconciliation.delegate_to:
key = Warning.format('delete.delegated', [reconciliation])
2019-04-12 19:43:53 +02:00
if Warning.check(key):
raise ReconciliationDeleteWarning(key,
2019-04-12 19:43:53 +02:00
gettext('account.msg_reconciliation_delete_delegated',
reconciliation=reconciliation.rec_name,
line=reconciliation.delegate_to.rec_name,
move=reconciliation.delegate_to.move.rec_name))
for line in reconciliation.lines:
if line.move.journal.type == 'write-off':
key = Warning.format('delete.write-off', [reconciliation])
if Warning.check(key):
raise ReconciliationDeleteWarning(key,
gettext(
'account.msg_reconciliation_delete_write_off',
reconciliation=reconciliation.rec_name,
line=line.rec_name,
move=line.move.rec_name))
2019-04-12 19:43:53 +02:00
super().delete(reconciliations)
2012-09-24 22:17:29 +02:00
@classmethod
def check_lines(cls, reconciliations):
Lang = Pool().get('ir.lang')
2012-09-24 22:17:29 +02:00
for reconciliation in reconciliations:
debit = Decimal(0)
credit = Decimal(0)
2008-03-19 22:13:51 +01:00
account = None
if reconciliation.lines:
party = reconciliation.lines[0].party
2008-03-19 22:13:51 +01:00
for line in reconciliation.lines:
if line.state != 'valid':
2018-12-29 14:20:28 +01:00
raise ReconciliationError(
gettext('account.msg_reconciliation_line_not_valid',
line=line.rec_name))
debit += line.debit
credit += line.credit
2008-03-19 22:13:51 +01:00
if not account:
account = line.account
elif account.id != line.account.id:
2018-12-29 14:20:28 +01:00
raise ReconciliationError(
gettext('account'
'.msg_reconciliation_different_accounts',
line=line.rec_name,
account1=line.account.rec_name,
account2=account.rec_name))
if not account.reconcile:
2018-12-29 14:20:28 +01:00
raise ReconciliationError(
gettext('account'
'.msg_reconciliation_account_not_reconcile',
line=line.rec_name,
account=line.account.rec_name))
if line.party != party:
2018-12-29 14:20:28 +01:00
raise ReconciliationError(
gettext('account'
'.msg_reconciliation_different_parties',
line=line.rec_name,
party1=line.party.rec_name,
party2=party.rec_name))
2021-03-21 22:38:05 +01:00
if not reconciliation.company.currency.is_zero(debit - credit):
2017-11-15 00:16:39 +01:00
lang = Lang.get()
2021-03-21 22:38:05 +01:00
debit = lang.currency(debit, reconciliation.company.currency)
credit = lang.currency(credit, reconciliation.company.currency)
2018-12-29 14:20:28 +01:00
raise ReconciliationError(
gettext('account.msg_reconciliation_unbalanced',
debit=debit,
credit=credit))
2008-03-19 22:13:51 +01:00
2021-10-11 23:21:55 +02:00
class MoveLineMixin:
__slots__ = ()
@classmethod
def get_move_origin(cls):
Move = Pool().get('account.move')
return Move.get_origin()
@classmethod
def get_move_states(cls):
pool = Pool()
Move = pool.get('account.move')
return Move.fields_get(['state'])['state']['selection']
def get_move_field(self, name):
field = getattr(self.__class__, name)
if name.startswith('move_'):
name = name[5:]
value = getattr(self.move, name)
if isinstance(value, ModelSQL):
if field._type == 'reference':
return str(value)
return value.id
return value
@classmethod
def set_move_field(cls, lines, name, value):
pool = Pool()
Move = pool.get('account.move')
if name.startswith('move_'):
name = name[5:]
if not value:
return
moves = {line.move for line in lines}
moves = Move.browse(moves)
Move.write(moves, {
name: value,
})
2021-10-11 23:21:55 +02:00
@classmethod
def search_move_field(cls, name, clause):
nested = clause[0][len(name):]
2021-10-11 23:21:55 +02:00
if name.startswith('move_'):
name = name[5:]
return [('move.' + name + nested, *clause[1:])]
2021-10-11 23:21:55 +02:00
@staticmethod
def _order_move_field(name):
def order_field(cls, tables):
pool = Pool()
Move = pool.get('account.move')
field = Move._fields[name]
table, _ = tables[None]
move_tables = tables.get('move')
if move_tables is None:
move = Move.__table__()
move_tables = {
None: (move, move.id == table.move),
}
tables['move'] = move_tables
return field.convert_order(name, move_tables, Move)
return classmethod(order_field)
def get_amount(self, name):
sign = -1 if self.account.type.statement == 'income' else 1
if self.amount_second_currency is not None:
return self.amount_second_currency * sign
else:
return (self.debit - self.credit) * sign
def get_amount_currency(self, name):
if self.second_currency:
currency = self.second_currency
else:
currency = self.account.currency
return currency.id
@classmethod
def get_payable_receivable_balance(cls, lines, name):
pool = Pool()
Account = pool.get('account.account')
AccountType = pool.get('account.account.type')
Move = pool.get('account.move')
transaction = Transaction()
database = transaction.database
context = transaction.context
cursor = transaction.connection.cursor()
line = cls.__table__()
account = Account.__table__()
account_type = AccountType.__table__()
move = Move.__table__()
balances = dict.fromkeys(map(int, lines))
for company, lines in groupby(lines, lambda l: l.company):
for sub_lines in grouped_slice(lines):
sub_lines = list(sub_lines)
where = account.company == company.id
where_type = Literal(False)
if context.get('receivable', True):
where_type |= account_type.receivable
if context.get('payable', True):
where_type |= account_type.payable
where &= where_type
if not context.get('reconciled'):
if hasattr(cls, 'reconciliation'):
where &= line.reconciliation == Null
elif hasattr(cls, 'reconciled'):
where &= ~line.reconciled
currency = Coalesce(line.second_currency, company.currency.id)
sign = Case(
(account_type.statement == 'income', -1),
else_=1)
balance = sign * Case(
(line.amount_second_currency != Null,
line.amount_second_currency),
else_=line.debit - line.credit)
if database.has_window_functions():
balance = Sum(balance,
window=Window(
[line.party, currency],
order_by=[
line.maturity_date.asc,
move.date.desc,
line.id.desc,
]))
party_where = Literal(False)
parties = {l.party for l in sub_lines}
if None in parties:
party_where |= line.party == parties.discard(None)
party_where |= reduce_ids(line.party, map(int, parties))
where &= party_where
query = (line
.join(move, condition=line.move == move.id)
.join(account, condition=line.account == account.id)
.join(
account_type,
condition=account.type == account_type.id)
.select(
line.id.as_('id'), balance.as_('balance'),
where=where))
cursor.execute(*query.select(
query.id, query.balance,
where=reduce_ids(query.id, [l.id for l in sub_lines])))
balances.update(cursor)
return balances
2021-10-11 23:21:55 +02:00
def get_rec_name(self, name):
if self.debit > self.credit:
return self.account.rec_name
else:
return '(%s)' % self.account.rec_name
@classmethod
def search_rec_name(cls, name, clause):
return [('account.rec_name',) + tuple(clause[1:])]
class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
2008-03-17 18:32:06 +01:00
'Account Move Line'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.line'
2016-06-30 18:50:40 +02:00
_states = {
'readonly': Eval('move_state') == 'posted',
}
2021-08-21 09:09:55 +02:00
debit = Monetary(
"Debit", currency='currency', digits='currency', required=True,
states=_states,
depends={'credit', 'tax_lines', 'journal'})
2021-08-21 09:09:55 +02:00
credit = Monetary(
"Credit", currency='currency', digits='currency', required=True,
states=_states,
depends={'debit', 'tax_lines', 'journal'})
2008-03-17 18:32:06 +01:00
account = fields.Many2One('account.account', 'Account', required=True,
2018-08-31 14:58:05 +02:00
domain=[
2021-04-10 13:55:35 +02:00
('company', '=', Eval('company', -1)),
2019-03-24 18:00:01 +01:00
('type', '!=', None),
('closed', '!=', True),
2018-08-31 14:58:05 +02:00
['OR',
('start_date', '=', None),
('start_date', '<=', Eval('date', None)),
],
['OR',
('end_date', '=', None),
('end_date', '>=', Eval('date', None)),
],
],
2021-04-10 13:55:35 +02:00
context={
'company': Eval('company', -1),
'period': Eval('period', -1),
2021-04-10 13:55:35 +02:00
},
states=_states, depends={'company', 'period'})
move = fields.Many2One(
'account.move', "Move", required=True,
2016-03-04 00:28:51 +01:00
ondelete='CASCADE',
2011-08-04 16:57:14 +02:00
states={
'required': False,
2016-06-30 18:50:40 +02:00
'readonly': (((Eval('state') == 'valid') | _states['readonly'])
& Bool(Eval('move'))),
})
2021-04-10 13:55:35 +02:00
journal = fields.Function(fields.Many2One(
'account.journal', 'Journal',
states=_states,
context={
'company': Eval('company', -1),
},
depends={'company'}),
2021-04-10 13:55:35 +02:00
'get_move_field', setter='set_move_field',
searcher='search_move_field')
2016-06-30 18:50:40 +02:00
period = fields.Function(fields.Many2One('account.period', 'Period',
states=_states),
2010-03-11 16:45:27 +01:00
'get_move_field', setter='set_move_field',
searcher='search_move_field')
2021-04-10 13:55:35 +02:00
company = fields.Function(fields.Many2One(
'company.company', "Company", states=_states),
2021-04-10 13:55:35 +02:00
'get_move_field', setter='set_move_field',
searcher='search_move_field')
2016-06-30 18:50:40 +02:00
date = fields.Function(fields.Date('Effective Date', required=True,
states=_states),
2018-08-31 14:58:05 +02:00
'on_change_with_date', setter='set_move_field',
2010-03-11 16:45:27 +01:00
searcher='search_move_field')
origin = fields.Reference(
"Origin", selection='get_origin', states=_states)
move_origin = fields.Function(
fields.Reference("Move Origin", selection='get_move_origin'),
2012-09-29 15:33:21 +02:00
'get_move_field', searcher='search_move_field')
description = fields.Char('Description', states=_states)
move_description_used = fields.Function(
fields.Char("Move Description", states=_states),
'get_move_field',
setter='set_move_field',
2014-02-03 18:12:16 +01:00
searcher='search_move_field')
2021-08-21 09:09:55 +02:00
amount_second_currency = Monetary(
"Amount Second Currency",
currency='second_currency', digits='second_currency',
states={
'required': Bool(Eval('second_currency')),
2016-06-30 18:50:40 +02:00
'readonly': _states['readonly'],
},
2021-08-21 09:09:55 +02:00
help='The amount expressed in a second currency.')
2008-05-09 17:24:55 +02:00
second_currency = fields.Many2One('currency.currency', 'Second Currency',
2019-12-18 09:16:34 +01:00
help='The second currency.',
domain=[
If(~Eval('second_currency_required'),
('id', '!=', Eval('currency', -1)),
('id', '=', Eval('second_currency_required', -1))),
],
states={
'required': (Bool(Eval('amount_second_currency'))
| Bool(Eval('second_currency_required'))),
2016-06-30 18:50:40 +02:00
'readonly': _states['readonly']
})
second_currency_required = fields.Function(
fields.Many2One('currency.currency', "Second Currency Required"),
'on_change_with_second_currency_required')
party = fields.Many2One(
'party.party', "Party",
states={
'required': Eval('party_required', False),
'invisible': ~Eval('party_required', False),
2016-06-30 18:50:40 +02:00
'readonly': _states['readonly'],
},
2021-04-10 13:55:35 +02:00
context={
'company': Eval('company', -1),
},
depends={'company'}, ondelete='RESTRICT')
2014-12-02 12:54:29 +01:00
party_required = fields.Function(fields.Boolean('Party Required'),
'on_change_with_party_required')
maturity_date = fields.Date(
"Maturity Date",
states={
'invisible': ~Eval('has_maturity_date'),
},
depends=['has_maturity_date'],
help="Set a date to make the line payable or receivable.")
has_maturity_date = fields.Function(
fields.Boolean("Has Maturity Date"),
'on_change_with_has_maturity_date')
2008-03-17 18:32:06 +01:00
state = fields.Selection([
('draft', 'Draft'),
('valid', 'Valid'),
], 'State', readonly=True, required=True, sort=False)
reconciliation = fields.Many2One(
'account.move.reconciliation', 'Reconciliation',
readonly=True, ondelete='SET NULL')
2019-04-12 19:43:53 +02:00
reconciliations_delegated = fields.One2Many(
'account.move.reconciliation', 'delegate_to',
"Reconciliations Delegated", readonly=True)
2008-03-23 23:05:12 +01:00
tax_lines = fields.One2Many('account.tax.line', 'move_line', 'Tax Lines')
move_state = fields.Function(
fields.Selection('get_move_states', "Move State"),
'on_change_with_move_state', searcher='search_move_field')
2020-10-09 19:09:05 +02:00
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"),
'on_change_with_currency', searcher='search_currency')
2021-08-21 09:09:55 +02:00
amount = fields.Function(Monetary(
"Amount", currency='amount_currency', digits='amount_currency'),
2015-02-10 00:13:10 +01:00
'get_amount')
amount_currency = fields.Function(fields.Many2One('currency.currency',
'Amount Currency'), 'get_amount_currency')
2022-04-11 19:19:54 +02:00
delegated_amount = fields.Function(Monetary(
"Delegated Amount",
currency='amount_currency', digits='amount_currency',
states={
2023-06-13 12:56:54 +02:00
'invisible': (
~Eval('reconciliation', False)
| ~Eval('delegated_amount', 0)),
2022-04-11 19:19:54 +02:00
}),
'get_delegated_amount')
payable_receivable_balance = fields.Function(
Monetary(
"Payable/Receivable Balance",
currency='amount_currency', digits='amount_currency'),
'get_payable_receivable_balance')
2008-03-17 18:32:06 +01:00
del _states
2016-06-30 18:50:40 +02:00
2012-09-24 22:17:29 +02:00
@classmethod
def __setup__(cls):
super(Line, cls).__setup__()
2021-03-07 11:13:53 +01:00
cls.__access__.add('move')
2018-04-30 15:48:54 +02:00
cls._check_modify_exclude = {
'maturity_date', 'reconciliation', 'tax_lines'}
cls._reconciliation_modify_disallow = {
'account', 'debit', 'credit', 'party',
}
table = cls.__table__()
2012-09-24 22:17:29 +02:00
cls._sql_constraints += [
2008-03-17 18:32:06 +01:00
('credit_debit',
Check(table, table.credit * table.debit == 0),
2018-12-29 14:20:28 +01:00
'account.msg_line_debit_credit'),
('second_currency_sign',
Check(table, Coalesce(table.amount_second_currency, 0)
* (table.debit - table.credit) >= 0),
2018-12-29 14:20:28 +01:00
'account.msg_line_second_currency_sign'),
2012-09-24 22:17:29 +02:00
]
cls.__rpc__.update({
'on_write': RPC(instantiate=0),
})
2020-12-16 21:31:25 +01:00
# Do not cache default_date nor default_move
cls.__rpc__['default_get'].cache = None
2012-09-24 22:17:29 +02:00
cls._order[0] = ('id', 'DESC')
cls._sql_indexes.update({
Index(
table,
(table.account, Index.Equality()),
(table.party, Index.Equality())),
Index(table, (table.reconciliation, Index.Equality())),
# Index for General Ledger
Index(
table,
(table.move, Index.Equality()),
(table.account, Index.Equality())),
# Index for account.account.party
Index(
table,
(table.account, Index.Equality()),
(table.party, Index.Equality()),
(table.id, Index.Equality()),
where=table.party != Null),
# Index for receivable/payable balance
Index(
table,
(table.account, Index.Equality()),
(table.party, Index.Equality()),
where=table.reconciliation == Null),
})
2012-09-24 22:17:29 +02:00
@classmethod
def default_date(cls):
2008-03-17 18:32:06 +01:00
'''
Return the date of the last line for journal, period
or the starting date of the period
or today
'''
2012-09-24 22:17:29 +02:00
pool = Pool()
Period = pool.get('account.period')
Date = pool.get('ir.date')
2023-02-14 01:32:04 +01:00
context = Transaction().context
2008-10-08 12:17:47 +02:00
2012-09-24 22:17:29 +02:00
date = Date.today()
lines = cls.search([
2023-02-14 01:32:04 +01:00
('company', '=', context.get('company')),
('journal', '=', context.get('journal')),
('period', '=', context.get('period')),
],
order=[('date', 'DESC')], limit=1)
2012-09-24 22:17:29 +02:00
if lines:
2023-02-14 01:32:04 +01:00
line, = lines
date = line.date
elif context.get('period'):
period = Period(context['period'])
if period.start_date >= date:
date = period.start_date
else:
date = period.end_date
if context.get('date'):
date = context['date']
2012-09-24 22:17:29 +02:00
return date
2008-03-17 18:32:06 +01:00
2017-07-27 00:03:37 +02:00
@classmethod
def default_move(cls):
transaction = Transaction()
context = transaction.context
if context.get('journal') and context.get('period'):
lines = cls.search([
2021-04-10 13:55:35 +02:00
('company', '=', context.get('company')),
2017-07-27 00:03:37 +02:00
('move.journal', '=', context['journal']),
('move.period', '=', context['period']),
('create_uid', '=', transaction.user),
('state', '=', 'draft'),
], order=[('id', 'DESC')], limit=1)
if lines:
line, = lines
return line.move.id
2021-04-10 13:55:35 +02:00
@fields.depends(
'move', 'debit', 'credit',
'_parent_move.lines', '_parent_move.company')
2017-07-27 00:03:37 +02:00
def on_change_move(self):
2021-04-10 13:55:35 +02:00
if self.move:
if not self.debit and not self.credit:
total = sum((l.debit or 0) - (l.credit or 0)
2021-04-10 13:55:35 +02:00
for l in getattr(self.move, 'lines', []))
self.debit = -total if total < 0 else Decimal(0)
self.credit = total if total > 0 else Decimal(0)
self.company = self.move.company
@classmethod
def default_company(cls):
return Transaction().context.get('company')
2017-07-27 00:03:37 +02:00
2012-09-24 22:17:29 +02:00
@staticmethod
def default_state():
2008-03-17 18:32:06 +01:00
return 'draft'
2012-09-24 22:17:29 +02:00
@staticmethod
def default_debit():
return Decimal(0)
2012-09-24 22:17:29 +02:00
@staticmethod
def default_credit():
return Decimal(0)
2020-10-09 19:09:05 +02:00
@fields.depends('account')
def on_change_with_currency(self, name=None):
2023-03-26 09:49:00 +02:00
return self.account.currency if self.account else None
2020-10-09 19:09:05 +02:00
@classmethod
def search_currency(cls, name, clause):
return [('account.company.' + clause[0], * clause[1:])]
@classmethod
def _get_origin(cls):
'Return list of Model names for origin Reference'
return ['account.move.line']
2012-09-29 15:33:21 +02:00
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
2020-12-27 00:19:30 +01:00
get_name = Model.get_name
models = cls._get_origin()
2020-12-27 00:19:30 +01:00
return [(None, '')] + [(m, get_name(m)) for m in models]
@property
def origin_rec_name(self):
if self.origin:
return self.origin.rec_name
elif self.move_origin:
return self.move_origin.rec_name
else:
return ''
2017-07-27 00:03:37 +02:00
@fields.depends('debit', 'credit', 'amount_second_currency')
2012-09-24 22:17:29 +02:00
def on_change_debit(self):
if self.debit:
self.credit = Decimal(0)
self._amount_second_currency_sign()
2017-07-27 00:03:37 +02:00
@fields.depends('debit', 'credit', 'amount_second_currency')
2012-09-24 22:17:29 +02:00
def on_change_credit(self):
if self.credit:
self.debit = Decimal(0)
self._amount_second_currency_sign()
@fields.depends('amount_second_currency', 'debit', 'credit')
def on_change_amount_second_currency(self):
self._amount_second_currency_sign()
def _amount_second_currency_sign(self):
'Set correct sign to amount_second_currency'
if self.amount_second_currency:
self.amount_second_currency = \
self.amount_second_currency.copy_sign(
(self.debit or 0) - (self.credit or 0))
2017-07-27 00:03:37 +02:00
@fields.depends('account')
2012-09-24 22:17:29 +02:00
def on_change_account(self):
if self.account:
if self.account.second_currency:
self.second_currency = self.account.second_currency
if not self.account.party_required:
self.party = None
@fields.depends('account')
def on_change_with_second_currency_required(self, name=None):
if self.account and self.account.second_currency:
return self.account.second_currency.id
@fields.depends('account')
def on_change_with_party_required(self, name=None):
if self.account:
return self.account.party_required
return False
2018-08-31 14:58:05 +02:00
@fields.depends('move', '_parent_move.date')
def on_change_with_date(self, name=None):
if self.move:
return self.move.date
2017-07-17 09:59:50 +02:00
@fields.depends('move', '_parent_move.state')
2016-06-30 18:50:40 +02:00
def on_change_with_move_state(self, name=None):
if self.move:
return self.move.state
@fields.depends('account')
def on_change_with_has_maturity_date(self, name=None):
if self.account:
type_ = self.account.type
return type_.receivable or type_.payable
2021-10-11 23:21:55 +02:00
order_journal = MoveLineMixin._order_move_field('journal')
order_period = MoveLineMixin._order_move_field('period')
order_company = MoveLineMixin._order_move_field('company')
order_date = MoveLineMixin._order_move_field('date')
order_move_origin = MoveLineMixin._order_move_field('origin')
order_move_state = MoveLineMixin._order_move_field('state')
2014-09-19 14:24:46 +02:00
2022-04-11 19:19:54 +02:00
def get_delegated_amount(self, name):
def final_delegated_line(line):
if not line.reconciliation or not line.reconciliation.delegate_to:
return line
return final_delegated_line(line.reconciliation.delegate_to)
final_delegation = final_delegated_line(self)
if final_delegation == self:
return None
elif final_delegation.reconciliation:
return final_delegation.amount_currency.round(0)
else:
return final_delegation.amount
2012-09-24 22:17:29 +02:00
@classmethod
2013-09-06 22:33:47 +02:00
def query_get(cls, table):
2008-03-17 18:32:06 +01:00
'''
2008-09-19 15:00:26 +02:00
Return SQL clause and fiscal years for account move line
depending of the context.
2013-09-06 22:33:47 +02:00
table is the SQL instance of account.move.line table
2008-03-17 18:32:06 +01:00
'''
2013-09-06 22:33:47 +02:00
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
Move = pool.get('account.move')
Period = pool.get('account.period')
move = Move.__table__()
period = Period.__table__()
2017-09-07 18:40:32 +02:00
fiscalyear = FiscalYear.__table__()
context = Transaction().context
company = context.get('company')
2017-09-07 18:40:32 +02:00
fiscalyear_ids = []
where = Literal(True)
if context.get('posted'):
where &= move.state == 'posted'
2021-01-04 21:33:28 +01:00
if context.get('journal'):
where &= move.journal == context['journal']
2017-09-07 18:40:32 +02:00
date = context.get('date')
from_date, to_date = context.get('from_date'), context.get('to_date')
fiscalyear_id = context.get('fiscalyear')
period_ids = context.get('periods')
if date:
2012-09-24 22:17:29 +02:00
fiscalyears = FiscalYear.search([
2017-09-07 18:40:32 +02:00
('start_date', '<=', date),
('end_date', '>=', date),
('company', '=', company),
],
order=[('start_date', 'DESC')],
limit=1)
2017-09-07 18:40:32 +02:00
if fiscalyears:
fiscalyear_id = fiscalyears[0].id
else:
2017-09-07 18:40:32 +02:00
fiscalyear_id = -1
2018-06-26 19:11:40 +02:00
fiscalyear_ids = list(map(int, fiscalyears))
2017-09-07 18:40:32 +02:00
where &= period.fiscalyear == fiscalyear_id
where &= move.date <= date
elif fiscalyear_id or period_ids is not None or from_date or to_date:
2017-09-07 18:40:32 +02:00
if fiscalyear_id:
fiscalyear_ids = [fiscalyear_id]
where &= fiscalyear.id == fiscalyear_id
if period_ids is not None:
where &= move.period.in_(period_ids or [None])
2017-09-07 18:40:32 +02:00
if from_date:
where &= move.date >= from_date
if to_date:
where &= move.date <= to_date
2008-03-17 18:32:06 +01:00
else:
2017-09-07 18:40:32 +02:00
where &= fiscalyear.state == 'open'
where &= fiscalyear.company == company
fiscalyears = FiscalYear.search([
('state', '=', 'open'),
2017-09-07 18:40:32 +02:00
('company', '=', company),
])
2018-06-26 19:11:40 +02:00
fiscalyear_ids = list(map(int, fiscalyears))
2017-09-07 18:40:32 +02:00
# Use LEFT JOIN to allow database optimization
# if no joined table is used in the where clause.
return (table.move.in_(move
2017-09-07 18:40:32 +02:00
.join(period, 'LEFT', condition=move.period == period.id)
.join(fiscalyear, 'LEFT',
condition=period.fiscalyear == fiscalyear.id)
.select(move.id, where=where)),
fiscalyear_ids)
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def on_write(cls, lines):
return list(set(l.id for line in lines for l in line.move.lines))
2008-03-17 18:32:06 +01:00
@classmethod
2022-04-11 23:24:20 +02:00
def validate_fields(cls, lines, field_names):
super(Line, cls).validate(lines)
2022-04-11 23:24:20 +02:00
cls.check_account(lines, field_names)
2022-04-11 23:24:20 +02:00
@classmethod
def check_account(cls, lines, field_names=None):
if field_names and not (field_names & {'account', 'party'}):
return
for line in lines:
if not line.account.type or line.account.closed:
raise AccessError(
gettext('account.msg_line_closed_account',
account=line.account.rec_name))
if bool(line.party) != bool(line.account.party_required):
error = 'party_set' if line.party else 'party_required'
raise AccessError(
gettext('account.msg_line_%s' % error,
account=line.account.rec_name,
line=line.rec_name))
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def check_journal_period_modify(cls, period, journal):
2008-03-17 18:32:06 +01:00
'''
Check if the lines can be modified or created for the journal - period
and if there is no journal - period, create it
'''
2012-09-24 22:17:29 +02:00
JournalPeriod = Pool().get('account.journal.period')
journal_periods = JournalPeriod.search([
('journal', '=', journal.id),
('period', '=', period.id),
], limit=1)
if journal_periods:
journal_period, = journal_periods
2023-06-20 10:13:56 +02:00
if journal_period.state == 'closed':
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account.msg_modify_line_closed_journal_period',
2018-12-29 14:20:28 +01:00
journal_period=journal_period.rec_name))
2008-03-17 18:32:06 +01:00
else:
JournalPeriod.lock()
2013-01-06 22:18:13 +01:00
JournalPeriod.create([{
'journal': journal.id,
'period': period.id,
}])
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
2014-09-21 12:12:26 +02:00
def check_modify(cls, lines, modified_fields=None):
2008-03-17 18:32:06 +01:00
'''
Check if the lines can be modified
'''
2014-09-21 12:12:26 +02:00
if (modified_fields is not None
and modified_fields <= cls._check_modify_exclude):
return
2008-03-17 18:32:06 +01:00
journal_period_done = []
2012-09-24 22:17:29 +02:00
for line in lines:
2008-03-17 18:32:06 +01:00
if line.move.state == 'posted':
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account.msg_modify_line_posted_move',
line=line.rec_name,
move=line.move.rec_name))
2008-03-17 18:32:06 +01:00
journal_period = (line.journal.id, line.period.id)
if journal_period not in journal_period_done:
2012-09-24 22:17:29 +02:00
cls.check_journal_period_modify(line.period,
line.journal)
2008-03-17 18:32:06 +01:00
journal_period_done.append(journal_period)
@classmethod
def check_reconciliation(cls, lines, modified_fields=None):
if (modified_fields is not None
and not modified_fields & cls._reconciliation_modify_disallow):
return
for line in lines:
if line.reconciliation:
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account.msg_modify_line_reconciled',
line=line.rec_name))
2022-04-11 20:13:16 +02:00
@classmethod
def view_attributes(cls):
attributes = super().view_attributes()
view_ids = cls._view_reconciliation_muted()
if Transaction().context.get('view_id') in view_ids:
attributes.append(
('/tree', 'visual',
If(Bool(Eval('reconciliation')), 'muted', '')))
return attributes
@classmethod
def _view_reconciliation_muted(cls):
pool = Pool()
ModelData = pool.get('ir.model.data')
return {ModelData.get_id(
'account', 'move_line_view_list_payable_receivable')}
2012-09-24 22:17:29 +02:00
@classmethod
def delete(cls, lines):
Move = Pool().get('account.move')
cls.check_modify(lines)
cls.check_reconciliation(lines)
2012-09-24 22:17:29 +02:00
moves = [x.move for x in lines]
super(Line, cls).delete(lines)
Move.validate_move(moves)
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
2013-12-19 14:44:33 +01:00
def write(cls, *args):
2012-09-24 22:17:29 +02:00
Move = Pool().get('account.move')
2013-12-19 14:44:33 +01:00
actions = iter(args)
args = []
moves = []
all_lines = []
for lines, values in zip(actions, actions):
2014-09-21 12:12:26 +02:00
cls.check_modify(lines, set(values.keys()))
cls.check_reconciliation(lines, set(values.keys()))
2013-12-19 14:44:33 +01:00
moves.extend((x.move for x in lines))
all_lines.extend(lines)
args.extend((lines, values))
super(Line, cls).write(*args)
2010-08-19 22:02:00 +02:00
Transaction().timestamp = {}
2013-12-19 14:44:33 +01:00
Move.validate_move(list(set(l.move for l in all_lines) | set(moves)))
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
2013-01-06 22:18:13 +01:00
def create(cls, vlist):
2012-09-24 22:17:29 +02:00
pool = Pool()
Move = pool.get('account.move')
def move_fields(move_name):
for fname, field in cls._fields.items():
if (isinstance(field, fields.Function)
and field.setter == 'set_move_field'):
if move_name and fname.startswith('move_'):
fname = fname[5:]
yield fname
moves = {}
context = Transaction().context
2013-01-06 22:18:13 +01:00
vlist = [x.copy() for x in vlist]
for vals in vlist:
if not vals.get('move'):
move_values = {}
for fname in move_fields(move_name=True):
move_values[fname] = vals.get(fname) or context.get(fname)
key = tuple(sorted(move_values.items()))
move = moves.get(key)
2016-06-17 13:40:45 +02:00
if move is None:
move = Move(**move_values)
2016-06-17 13:40:45 +02:00
move.save()
moves[key] = move
2016-06-17 13:40:45 +02:00
vals['move'] = move.id
2013-01-06 22:18:13 +01:00
else:
# prevent default value for field with set_move_field
for fname in move_fields(move_name=False):
vals.setdefault(fname, None)
2013-01-06 22:18:13 +01:00
lines = super(Line, cls).create(vlist)
2013-02-16 18:54:35 +01:00
period_and_journals = set((line.period, line.journal)
2013-01-06 22:18:13 +01:00
for line in lines)
for period, journal in period_and_journals:
cls.check_journal_period_modify(period, journal)
# Re-browse for cache alignment
moves = Move.browse(list(set(line.move for line in lines)))
Move.check_modify(moves)
Move.validate_move(moves)
2013-01-06 22:18:13 +01:00
return lines
2008-03-17 18:32:06 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
default.setdefault('move', None)
default.setdefault('reconciliation', None)
default.setdefault('reconciliations_delegated', [])
2012-09-24 22:17:29 +02:00
return super(Line, cls).copy(lines, default=default)
2015-03-02 23:34:41 +01:00
@classmethod
def view_toolbar_get(cls):
pool = Pool()
Template = pool.get('account.move.template')
toolbar = super(Line, cls).view_toolbar_get()
# Add a wizard entry for each templates
context = Transaction().context
company = context.get('company')
journal = context.get('journal')
period = context.get('period')
if company and journal and period:
templates = Template.search([
('company', '=', company),
('journal', '=', journal),
])
for template in templates:
action = toolbar['action']
# Use template id for action id to auto-select the template
action.append({
'name': template.name,
'type': 'ir.action.wizard',
'wiz_name': 'account.move.template.create',
'id': template.id,
})
return toolbar
2012-09-24 22:17:29 +02:00
@classmethod
2018-09-01 11:20:13 +02:00
def reconcile(
2019-04-12 19:43:53 +02:00
cls, *lines_list, date=None, writeoff=None, description=None,
delegate_to=None):
"""
Reconcile each list of lines together.
2018-09-01 11:20:13 +02:00
The writeoff keys are: date, method and description.
"""
2011-07-03 13:18:40 +02:00
pool = Pool()
2012-09-24 22:17:29 +02:00
Reconciliation = pool.get('account.move.reconciliation')
Move = pool.get('account.move')
Lang = pool.get('ir.lang')
lang = Lang.get()
2019-04-12 19:43:53 +02:00
delegate_to = delegate_to.id if delegate_to else None
reconciliations = []
to_post = []
for lines in lines_list:
2023-10-21 15:46:53 +02:00
if not lines:
continue
for line in lines:
if line.reconciliation:
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account.msg_line_already_reconciled',
line=line.rec_name))
lines = list(lines)
reconcile_account = None
reconcile_party = None
amount = Decimal(0)
amount_second_currency = Decimal(0)
second_currencies = set()
posted = True
for line in lines:
posted &= line.move.state == 'posted'
amount += line.debit - line.credit
if not reconcile_account:
reconcile_account = line.account
if not reconcile_party:
reconcile_party = line.party
if line.amount_second_currency is not None:
amount_second_currency += line.amount_second_currency
second_currencies.add(line.second_currency)
company = reconcile_account.company
try:
second_currency, = second_currencies
except ValueError:
amount_second_currency = None
second_currency = None
if second_currency:
writeoff_amount = amount_second_currency
writeoff_currency = second_currency
else:
writeoff_amount = amount
writeoff_currency = company.currency
if writeoff_amount:
if not writeoff:
raise ReconciliationError(gettext(
'account.msg_reconciliation_write_off_missing',
amount=lang.currency(
writeoff_amount, writeoff_currency)))
move = cls._get_writeoff_move(
reconcile_account, reconcile_party,
writeoff_amount, writeoff_currency,
writeoff, date=date, description=description)
move.save()
if posted:
to_post.append(move)
for line in move.lines:
if line.account == reconcile_account:
lines.append(line)
amount += line.debit - line.credit
if second_currency and amount:
move = cls._get_exchange_move(
reconcile_account, reconcile_party, amount, date)
move.save()
if posted:
to_post.append(move)
for line in move.lines:
if line.account == reconcile_account:
lines.append(line)
amount += line.debit - line.credit
assert not amount, f"{amount} must be zero"
reconciliations.append({
2021-03-21 22:38:05 +01:00
'company': reconcile_account.company,
'lines': [('add', [x.id for x in lines])],
'date': max(l.date for l in lines),
2019-04-12 19:43:53 +02:00
'delegate_to': delegate_to,
})
if to_post:
Move.post(to_post)
return Reconciliation.create(reconciliations)
@classmethod
def _get_writeoff_move(
cls, reconcile_account, reconcile_party, amount, currency,
writeoff, date=None, description=None):
pool = Pool()
Currency = pool.get('currency.currency')
2012-09-24 22:17:29 +02:00
Date = pool.get('ir.date')
Period = pool.get('account.period')
Move = pool.get('account.move')
company = reconcile_account.company
if not date:
with Transaction().set_context(company=company.id):
date = Date.today()
period = Period.find(reconcile_account.company, date=date)
if amount >= 0:
account = writeoff.debit_account
else:
account = writeoff.credit_account
journal = writeoff.journal
if currency != company.currency:
amount_second_currency = amount
second_currency = currency
with Transaction().set_context(date=date):
amount = Currency.compute(
currency, amount, company.currency)
else:
amount_second_currency = None
second_currency = None
move = Move()
move.company = company
move.journal = journal
move.period = period
move.date = date
move.description = description
lines = []
line = cls()
lines.append(line)
line.account = reconcile_account
line.party = reconcile_party
line.debit = -amount if amount < 0 else 0
line.credit = amount if amount > 0 else 0
if second_currency:
line.amount_second_currency = (
amount_second_currency).copy_sign(
line.debit - line.credit)
line.second_currency = second_currency
line = cls()
lines.append(line)
line.account = account
line.party = (
reconcile_party if account.party_required else None)
line.debit = amount if amount > 0 else 0
line.credit = -amount if amount < 0 else 0
if account.second_currency:
with Transaction().set_context(date=date):
line.amount_second_currency = Currency.compute(
company.currency, amount,
reconcile_account.second_currency).copy_sign(
line.debit - line.credit)
move.lines = lines
return move
@classmethod
def _get_exchange_move(cls, account, party, amount, date=None):
pool = Pool()
Configuration = pool.get('account.configuration')
Date = pool.get('ir.date')
Move = pool.get('account.move')
Period = pool.get('account.period')
configuration = Configuration(1)
company = account.company
if not date:
with Transaction().set_context(company=company.id):
date = Date.today()
period_id = Period.find(company.id, date=date)
move = Move()
move.company = company
move.journal = configuration.get_multivalue(
'currency_exchange_journal', company=company.id)
if not move.journal:
raise ReconciliationError(gettext(
'account.'
'msg_reconciliation_currency_exchange_journal_missing',
company=company.rec_name))
move.period = period_id
move.date = date
lines = []
line = cls()
lines.append(line)
line.account = account
line.party = party
line.debit = -amount if amount < 0 else 0
line.credit = amount if amount > 0 else 0
line = cls()
lines.append(line)
line.debit = amount if amount > 0 else 0
line.credit = -amount if amount < 0 else 0
if line.credit:
line.account = configuration.get_multivalue(
'currency_exchange_credit_account', company=company.id)
if not line.account:
raise ReconciliationError(gettext(
'account.'
'msg_reconciliation_currency_exchange_'
'credit_account_missing',
company=company.rec_name))
else:
line.account = configuration.get_multivalue(
'currency_exchange_debit_account', company=company.id)
if not line.account:
raise ReconciliationError(gettext(
'account.'
'msg_reconciliation_currency_exchange_'
'debit_account_missing',
company=company.rec_name))
move.lines = lines
return move
2008-03-17 18:32:06 +01:00
class LineReceivablePayableContext(ModelView):
"Receivable/Payable Line Context"
__name__ = 'account.move.line.receivable_payable.context'
reconciled = fields.Boolean("Reconciled")
receivable = fields.Boolean("Receivable")
payable = fields.Boolean("Payable")
@classmethod
def default_reconciled(cls):
return Transaction().context.get('reconciled', False)
@classmethod
def default_receivable(cls):
return Transaction().context.get('receivable', True)
@classmethod
def default_payable(cls):
return Transaction().context.get('payable', True)
2018-09-01 11:20:13 +02:00
class WriteOff(DeactivableMixin, ModelSQL, ModelView):
'Reconcile Write Off'
__name__ = 'account.move.reconcile.write_off'
company = fields.Many2One('company.company', "Company", required=True)
name = fields.Char("Name", required=True, translate=True)
journal = fields.Many2One('account.journal', "Journal", required=True,
domain=[('type', '=', 'write-off')],
context={
'company': Eval('company', -1),
},
depends={'company'})
2018-09-01 11:20:13 +02:00
credit_account = fields.Many2One('account.account', "Credit Account",
required=True,
domain=[
2019-03-24 18:00:01 +01:00
('type', '!=', None),
('closed', '!=', True),
2018-09-01 11:20:13 +02:00
('company', '=', Eval('company')),
])
2018-09-01 11:20:13 +02:00
debit_account = fields.Many2One('account.account', "Debit Account",
required=True,
domain=[
2019-03-24 18:00:01 +01:00
('type', '!=', None),
('closed', '!=', True),
2018-09-01 11:20:13 +02:00
('company', '=', Eval('company')),
])
2018-09-01 11:20:13 +02:00
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('name', 'ASC'))
2018-09-01 11:20:13 +02:00
@classmethod
def default_company(cls):
return Transaction().context.get('company')
2009-02-28 16:57:10 +01:00
class OpenJournalAsk(ModelView):
'Open Journal Ask'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.open_journal.ask'
2008-03-17 18:32:06 +01:00
journal = fields.Many2One('account.journal', 'Journal', required=True)
period = fields.Many2One('account.period', 'Period', required=True,
2011-08-21 11:20:29 +02:00
domain=[
2023-06-20 10:13:56 +02:00
('state', '!=', 'closed'),
2010-01-20 10:56:33 +01:00
])
2008-03-17 18:32:06 +01:00
@classmethod
def default_period(cls):
pool = Pool()
Period = pool.get('account.period')
company = Transaction().context.get('company')
try:
period = Period.find(company)
except PeriodNotFoundError:
return None
return period.id
2008-03-17 18:32:06 +01:00
class OpenJournal(Wizard):
'Open Journal'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.open_journal'
2012-01-02 17:54:33 +01:00
start = StateTransition()
ask = StateView('account.move.open_journal.ask',
'account.open_journal_ask_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'open_', 'tryton-ok', default=True),
])
open_ = StateAction('account.act_move_line_form')
2012-09-24 22:17:29 +02:00
def transition_start(self):
if (self.model
and self.model.__name__ == 'account.journal.period'
and self.record):
2012-01-02 17:54:33 +01:00
return 'open_'
return 'ask'
2012-09-24 22:17:29 +02:00
def default_ask(self, fields):
if (self.model
and self.model.__name__ == 'account.journal.period'
and self.record):
return {
'journal': self.record.journal.id,
'period': self.record.period.id,
2012-09-24 22:17:29 +02:00
}
return {}
2012-09-24 22:17:29 +02:00
def do_open_(self, action):
JournalPeriod = Pool().get('account.journal.period')
2012-01-02 17:54:33 +01:00
if (self.model
and self.model.__name__ == 'account.journal.period'
and self.record):
journal = self.record.journal
period = self.record.period
else:
2012-09-24 22:17:29 +02:00
journal = self.ask.journal
period = self.ask.period
journal_periods = JournalPeriod.search([
('journal', '=', journal.id),
('period', '=', period.id),
], limit=1)
if not journal_periods:
2013-01-06 22:18:13 +01:00
journal_period, = JournalPeriod.create([{
'journal': journal.id,
'period': period.id,
}])
2012-01-02 17:54:33 +01:00
else:
2012-09-24 22:17:29 +02:00
journal_period, = journal_periods
2012-01-02 17:54:33 +01:00
action['name'] += ' (%s)' % journal_period.rec_name
2012-01-02 17:54:33 +01:00
action['pyson_domain'] = PYSONEncoder().encode([
2012-09-24 22:17:29 +02:00
('journal', '=', journal.id),
('period', '=', period.id),
2021-04-10 13:55:35 +02:00
('company', '=', period.company.id),
2012-01-02 17:54:33 +01:00
])
action['pyson_context'] = PYSONEncoder().encode({
2012-09-24 22:17:29 +02:00
'journal': journal.id,
'period': period.id,
2021-04-10 13:55:35 +02:00
'company': period.company.id,
2008-07-29 12:14:16 +02:00
})
2012-01-02 17:54:33 +01:00
return action, {}
2012-09-24 22:17:29 +02:00
def transition_open_(self):
2012-01-02 17:54:33 +01:00
return 'end'
2008-03-17 18:32:06 +01:00
2008-03-18 13:20:12 +01:00
class OpenAccount(Wizard):
'Open Account'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.open_account'
2012-01-02 17:54:33 +01:00
start_state = 'open_'
open_ = StateAction('account.act_move_line_form')
2008-03-18 13:20:12 +01:00
2012-09-24 22:17:29 +02:00
def do_open_(self, action):
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
context = Transaction().context
2008-03-18 13:20:12 +01:00
company_id = self.record.company.id if self.record else -1
date = context.get('date')
fiscalyear = context.get('fiscalyear')
if date:
fiscalyears = FiscalYear.search([
('start_date', '<=', date),
('end_date', '>=', date),
('company', '=', company_id),
],
order=[('start_date', 'DESC')],
limit=1)
elif fiscalyear:
fiscalyears = [FiscalYear(fiscalyear)]
else:
2012-09-24 22:17:29 +02:00
fiscalyears = FiscalYear.search([
('state', '=', 'open'),
('company', '=', company_id),
2012-09-24 22:17:29 +02:00
])
periods = [p for f in fiscalyears for p in f.periods]
2008-03-18 13:20:12 +01:00
2012-01-02 17:54:33 +01:00
action['pyson_domain'] = [
2012-09-24 22:17:29 +02:00
('period', 'in', [p.id for p in periods]),
('account', '=', self.record.id if self.record else None),
('state', '=', 'valid'),
2008-03-28 17:39:54 +01:00
]
2010-08-19 22:02:00 +02:00
if Transaction().context.get('posted'):
2012-01-02 17:54:33 +01:00
action['pyson_domain'].append(('move.state', '=', 'posted'))
2012-08-16 15:55:45 +02:00
if Transaction().context.get('date'):
action['pyson_domain'].append(('move.date', '<=',
Transaction().context['date']))
if self.record:
action['name'] += ' (%s)' % self.record.rec_name
2012-01-02 17:54:33 +01:00
action['pyson_domain'] = PYSONEncoder().encode(action['pyson_domain'])
action['pyson_context'] = PYSONEncoder().encode({
2010-08-19 22:02:00 +02:00
'fiscalyear': Transaction().context.get('fiscalyear'),
2010-01-20 10:56:33 +01:00
})
2012-01-02 17:54:33 +01:00
return action, {}
2008-03-18 13:20:12 +01:00
2008-03-18 23:40:57 +01:00
2009-02-28 16:57:10 +01:00
class ReconcileLinesWriteOff(ModelView):
2008-03-19 22:13:51 +01:00
'Reconcile Lines Write-Off'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.reconcile_lines.writeoff'
2018-09-01 11:20:13 +02:00
company = fields.Many2One('company.company', "Company", readonly=True)
writeoff = fields.Many2One('account.move.reconcile.write_off', "Write Off",
required=True,
2013-12-20 13:59:46 +01:00
domain=[
2018-09-01 11:20:13 +02:00
('company', '=', Eval('company')),
])
date = fields.Date('Date', required=True)
2021-08-21 09:09:55 +02:00
amount = Monetary(
"Amount", currency='currency', digits='currency', readonly=True)
currency = fields.Many2One('currency.currency', "Currency", readonly=True)
2013-11-28 23:26:15 +01:00
description = fields.Char('Description')
2008-03-19 22:13:51 +01:00
2012-09-24 22:17:29 +02:00
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
2008-03-19 22:13:51 +01:00
class ReconcileLines(Wizard):
'Reconcile Lines'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.reconcile_lines'
2012-01-02 17:54:33 +01:00
start = StateTransition()
writeoff = StateView('account.move.reconcile_lines.writeoff',
'account.reconcile_lines_writeoff_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Reconcile', 'reconcile', 'tryton-ok', default=True),
])
reconcile = StateTransition()
@classmethod
def check_access(cls, models=None):
transaction = Transaction()
if transaction.user:
context = transaction.context
models = set() if models is None else models.copy()
models.update({'account.move.line', 'account.general_ledger.line'})
model = context.get('active_model')
if model and model not in models:
raise AccessError(gettext(
'ir.msg_access_wizard_model_error',
wizard=cls.__name__,
model=model))
super().check_access()
2013-08-21 18:19:26 +02:00
def get_writeoff(self):
"Return writeoff amount and company"
2024-03-02 11:39:29 +01:00
company = currency = None
amount = Decimal(0)
amount_second_currency = Decimal(0)
second_currencies = set()
for line in self.records:
2008-03-19 22:13:51 +01:00
amount += line.debit - line.credit
if line.amount_second_currency is not None:
amount_second_currency += line.amount_second_currency
second_currencies.add(line.second_currency)
2008-03-19 22:13:51 +01:00
if not company:
company = line.account.company
2024-03-02 11:39:29 +01:00
currency = company.currency
try:
second_currency, = second_currencies
except ValueError:
second_currency = None
if second_currency:
amount = amount_second_currency
currency = second_currency
return amount, currency, company
2013-08-21 18:19:26 +02:00
def transition_start(self):
amount, currency, company = self.get_writeoff()
if not company:
return 'end'
if currency.is_zero(amount):
2008-03-19 22:13:51 +01:00
return 'reconcile'
return 'writeoff'
2013-08-21 18:19:26 +02:00
def default_writeoff(self, fields):
amount, currency, company = self.get_writeoff()
2013-08-21 18:19:26 +02:00
return {
'amount': amount,
'currency': currency.id,
2018-09-01 11:20:13 +02:00
'company': company.id,
2013-08-21 18:19:26 +02:00
}
2012-09-24 22:17:29 +02:00
def transition_reconcile(self):
self.model.reconcile(
self.records,
writeoff=getattr(self.writeoff, 'writeoff', None),
date=getattr(self.writeoff, 'date', None),
description=getattr(self.writeoff, 'description', None))
2012-01-02 17:54:33 +01:00
return 'end'
2008-03-19 22:13:51 +01:00
class UnreconcileLines(Wizard):
'Unreconcile Lines'
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.unreconcile_lines'
2012-03-25 00:17:23 +01:00
start_state = 'unreconcile'
2012-01-02 17:54:33 +01:00
unreconcile = StateTransition()
@classmethod
def check_access(cls, models=None):
transaction = Transaction()
if transaction.user:
context = transaction.context
models = set() if models is None else models.copy()
models.update({'account.move.line', 'account.general_ledger.line'})
model = context.get('active_model')
if model and model not in models:
raise AccessError(gettext(
'ir.msg_access_wizard_model_error',
wizard=cls.__name__,
model=model))
super().check_access()
2012-09-24 22:17:29 +02:00
def transition_unreconcile(self):
self.make_unreconciliation(self.records)
2019-11-18 12:24:37 +01:00
return 'end'
@classmethod
def make_unreconciliation(cls, lines):
pool = Pool()
Reconciliation = pool.get('account.move.reconciliation')
2012-09-24 22:17:29 +02:00
reconciliations = [x.reconciliation for x in lines if x.reconciliation]
if reconciliations:
Reconciliation.delete(reconciliations)
2014-09-19 16:24:48 +02:00
class Reconcile(Wizard):
'Reconcile'
__name__ = 'account.reconcile'
2022-04-11 21:09:28 +02:00
start = StateView(
'account.reconcile.start',
'account.reconcile_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Reconcile", 'next_', 'tryton-ok', default=True),
])
2014-09-19 16:24:48 +02:00
next_ = StateTransition()
show = StateView('account.reconcile.show',
'account.reconcile_show_view_form', [
2012-01-02 17:54:33 +01:00
Button('Cancel', 'end', 'tryton-cancel'),
Button('Skip', 'next_', 'tryton-forward', validate=False),
2014-09-19 16:24:48 +02:00
Button('Reconcile', 'reconcile', 'tryton-ok', default=True),
2012-01-02 17:54:33 +01:00
])
2014-09-19 16:24:48 +02:00
reconcile = StateTransition()
2012-01-02 17:54:33 +01:00
2014-09-19 16:24:48 +02:00
def get_accounts(self):
'Return a list of account id to reconcile'
pool = Pool()
Rule = pool.get('ir.rule')
2014-09-19 16:24:48 +02:00
Line = pool.get('account.move.line')
line = Line.__table__()
Account = pool.get('account.account')
2019-03-24 18:00:01 +01:00
AccountType = pool.get('account.account.type')
2014-09-19 16:24:48 +02:00
account = Account.__table__()
2019-03-24 18:00:01 +01:00
account_type = AccountType.__table__()
cursor = Transaction().connection.cursor()
account_rule = Rule.query_get(Account.__name__)
2014-09-19 16:24:48 +02:00
if self.model and self.model.__name__ == 'account.move.line':
lines = [l for l in self.records if not l.reconciliation]
return list({l.account for l in lines if l.account.reconcile})
2014-09-19 16:24:48 +02:00
balance = line.debit - line.credit
cursor.execute(*line.join(account,
2019-03-24 18:00:01 +01:00
condition=line.account == account.id)
.join(account_type, condition=account.type == account_type.id)
.select(
2014-09-19 16:24:48 +02:00
account.id,
where=((line.reconciliation == Null) & account.reconcile
& account.id.in_(account_rule)),
2019-03-24 18:00:01 +01:00
group_by=[account.id,
account_type.receivable, account_type.payable],
having=((
Sum(Case((balance > 0, 1), else_=0)) > 0)
& (Sum(Case((balance < 0, 1), else_=0)) > 0)
2019-03-24 18:00:01 +01:00
| Case((account_type.receivable & ~account_type.payable,
Sum(balance) < 0),
else_=False)
| Case((account_type.payable & ~account_type.receivable,
Sum(balance) > 0),
else_=False)
)))
return [a for a, in cursor]
2014-09-19 16:24:48 +02:00
def get_parties(self, account, party=None):
2014-09-19 16:24:48 +02:00
'Return a list party to reconcile for the account'
pool = Pool()
Line = pool.get('account.move.line')
line = Line.__table__()
cursor = Transaction().connection.cursor()
2014-09-19 16:24:48 +02:00
if self.model and self.model.__name__ == 'account.move.line':
lines = [l for l in self.records if not l.reconciliation]
return list({l.party for l in lines if l.account == account})
where = ((line.reconciliation == Null)
& (line.account == account.id))
if party:
where &= (line.party == party.id)
cursor.execute(*line.select(line.party,
where=where,
group_by=line.party))
return [p for p, in cursor]
def get_currencies(self, account, party, currency=None, _balanced=False):
"Return a list of currencies to reconcile for the account and party"
pool = Pool()
Line = pool.get('account.move.line')
line = Line.__table__()
cursor = Transaction().connection.cursor()
if self.model and self.model.__name__ == 'account.move.line':
lines = [l for l in self.records if not l.reconciliation]
return list({
(l.second_currency or l.currency).id
for l in lines
if l.account == account and l.party == party})
balance = Case(
(line.second_currency != Null, line.amount_second_currency),
else_=line.debit - line.credit)
2020-03-28 16:54:08 +01:00
if _balanced:
having = Sum(balance) == 0
else:
having = ((
Sum(Case((balance > 0, 1), else_=0)) > 0)
& (Sum(Case((balance < 0, 1), else_=0)) > 0)
| Case((account.type.receivable, Sum(balance) < 0),
else_=False)
| Case((account.type.payable, Sum(balance) > 0),
else_=False)
)
where = ((line.reconciliation == Null)
& (line.account == account.id)
& (line.party == (party.id if party else None)))
currency_expr = Coalesce(line.second_currency, account.currency.id)
if currency:
where &= (currency_expr == currency.id)
cursor.execute(*line.select(
currency_expr,
where=where,
group_by=line.second_currency,
2020-03-28 16:54:08 +01:00
having=having))
return [p for p, in cursor]
2014-09-19 16:24:48 +02:00
@check_access
2014-09-19 16:24:48 +02:00
def transition_next_(self):
2022-04-11 21:09:28 +02:00
pool = Pool()
Line = pool.get('account.move.line')
2014-09-19 16:24:48 +02:00
def next_account():
accounts = list(self.show.accounts)
if not accounts:
return
account = accounts.pop()
self.show.account = account
self.show.accounts = accounts
self.show.parties = self.get_parties(account)
2014-09-19 16:24:48 +02:00
return account
def next_party():
parties = list(self.show.parties)
if not parties:
return
party = parties.pop()
self.show.party = party
self.show.parties = parties
self.show.currencies = self.get_currencies(
self.show.account, party)
return party,
2014-09-19 16:24:48 +02:00
def next_currency():
currencies = list(self.show.currencies)
if not currencies:
return
currency = currencies.pop()
self.show.currency = currency
self.show.currencies = currencies
return currency
if getattr(self.show, 'accounts', None) is None:
self.show.accounts = self.get_accounts()
if not (next_account() and next_party()):
return 'end'
if getattr(self.show, 'parties', None) is None:
self.show.parties = self.get_parties(self.show.account)
if not next_party():
return 'end'
if getattr(self.show, 'currencies', None) is None:
self.show.currencies = self.get_currencies(
self.show.account, self.show.party)
while not next_currency():
while not next_party():
if not next_account():
return 'end'
if self.start.automatic or self.start.only_balanced:
lines = self._default_lines()
if lines and self.start.automatic:
while lines:
Line.reconcile(lines)
lines = self._default_lines()
if not self.get_currencies(
self.show.account, self.show.party,
currency=self.show.currency):
2022-04-11 21:09:28 +02:00
return 'next_'
elif not lines and self.start.only_balanced:
return 'next_'
return 'show'
2014-09-19 16:24:48 +02:00
def default_show(self, fields):
pool = Pool()
Date = pool.get('ir.date')
defaults = {}
defaults['accounts'] = [a.id for a in self.show.accounts]
defaults['account'] = self.show.account.id
defaults['company'] = self.show.account.company.id
2014-09-19 16:24:48 +02:00
defaults['parties'] = [p.id for p in self.show.parties]
defaults['party'] = self.show.party.id if self.show.party else None
defaults['currencies'] = [c.id for c in self.show.currencies]
defaults['currency'] = (
self.show.currency.id if self.show.currency else None)
2022-04-11 21:09:28 +02:00
defaults['lines'] = list(map(int, self._default_lines()))
defaults['write_off_amount'] = None
with Transaction().set_context(company=self.show.account.company.id):
defaults['date'] = Date.today()
2014-09-19 16:24:48 +02:00
return defaults
def _all_lines(self):
'Return all lines to reconcile for the current state'
pool = Pool()
Line = pool.get('account.move.line')
return Line.search([
('account', '=', self.show.account.id),
('party', '=',
self.show.party.id if self.show.party else None),
('reconciliation', '=', None),
['OR',
[
('currency', '=', self.show.currency.id),
('second_currency', '=', None),
],
('second_currency', '=', self.show.currency.id),
],
],
order=[])
def _line_sort_key(self, line):
return [line.maturity_date or line.date]
2014-09-19 16:24:48 +02:00
def _default_lines(self):
'Return the larger list of lines which can be reconciled'
if self.model and self.model.__name__ == 'account.move.line':
requested = {
l for l in self.records
if l.account == self.show.account
and l.party == self.show.party
and (l.second_currency or l.currency) == self.show.currency}
else:
requested = None
currency = self.show.currency
def balance(line):
if line.second_currency == currency:
return line.amount_second_currency
elif line.currency == currency:
return line.debit - line.credit
else:
return 0
2022-01-30 13:05:00 +01:00
all_lines = self._all_lines()
amount = sum(map(balance, all_lines))
2022-01-30 13:05:00 +01:00
if currency.is_zero(amount):
2022-04-11 21:09:28 +02:00
return all_lines
2022-01-30 13:05:00 +01:00
2015-03-22 03:19:42 +01:00
chunk = config.getint('account', 'reconciliation_chunk', default=10)
2014-09-19 16:24:48 +02:00
# Combination is exponential so it must be limited to small number
default = []
for lines in grouped_slice(
2022-01-30 13:05:00 +01:00
sorted(all_lines, key=self._line_sort_key), chunk):
2014-09-19 16:24:48 +02:00
lines = list(lines)
best = None
2018-06-26 19:11:40 +02:00
for n in range(len(lines), 1, -1):
2014-09-19 16:24:48 +02:00
for comb_lines in combinations(lines, n):
if requested and not requested.intersection(comb_lines):
continue
amount = sum(balance(l) for l in comb_lines)
2014-09-19 16:24:48 +02:00
if currency.is_zero(amount):
2022-04-11 21:09:28 +02:00
best = comb_lines
2014-09-19 16:24:48 +02:00
break
if best:
break
if best:
default.extend(best)
if not default and requested:
2022-04-11 21:09:28 +02:00
return requested
else:
return default
2014-09-19 16:24:48 +02:00
def transition_reconcile(self):
pool = Pool()
Line = pool.get('account.move.line')
if self.show.lines:
Line.reconcile(self.show.lines,
date=self.show.date,
writeoff=self.show.write_off,
2014-09-19 16:24:48 +02:00
description=self.show.description)
if self.get_currencies(
self.show.account, self.show.party,
currency=self.show.currency):
return 'show'
2014-09-19 16:24:48 +02:00
return 'next_'
2022-04-11 21:09:28 +02:00
class ReconcileStart(ModelView):
"Reconcile"
__name__ = 'account.reconcile.start'
automatic = fields.Boolean(
"Automatic",
help="Automatically reconcile suggestions.")
only_balanced = fields.Boolean(
"Only Balanced",
help="Skip suggestion with write-off.")
@classmethod
def default_automatic(cls):
return False
@classmethod
def default_only_balanced(cls):
return False
2014-09-19 16:24:48 +02:00
class ReconcileShow(ModelView):
'Reconcile'
__name__ = 'account.reconcile.show'
company = fields.Many2One('company.company', "Company", readonly=True)
accounts = fields.Many2Many('account.account', None, None, 'Account',
2014-09-19 16:24:48 +02:00
readonly=True)
account = fields.Many2One('account.account', 'Account', readonly=True)
parties = fields.Many2Many(
'party.party', None, None, 'Parties', readonly=True,
context={
'company': Eval('company', -1),
},
depends={'company'})
party = fields.Many2One(
'party.party', 'Party', readonly=True,
context={
'company': Eval('company', -1),
},
depends={'company'})
currencies = fields.Many2Many(
'currency.currency', None, None, "Currencies", readonly=True)
currency = fields.Many2One('currency.currency', "Currency", readonly=True)
2014-09-19 16:24:48 +02:00
lines = fields.Many2Many('account.move.line', None, None, 'Lines',
domain=[
('account', '=', Eval('account')),
('party', '=', Eval('party')),
2012-03-27 10:43:03 +02:00
('reconciliation', '=', None),
['OR',
[
('currency', '=', Eval('currency', -1)),
('second_currency', '=', None),
],
('second_currency', '=', Eval('currency', -1)),
],
])
2014-09-19 16:24:48 +02:00
_write_off_states = {
'required': Bool(Eval('write_off_amount', 0)),
'invisible': ~Eval('write_off_amount', 0),
2014-09-19 16:24:48 +02:00
}
write_off_amount = Monetary(
"Amount", currency='currency', digits='currency', readonly=True,
states=_write_off_states)
write_off = fields.Many2One(
'account.move.reconcile.write_off', "Write Off",
domain=[
('company', '=', Eval('company', -1)),
],
states=_write_off_states)
date = fields.Date('Date', states=_write_off_states)
2014-09-19 16:24:48 +02:00
description = fields.Char('Description',
states={
'invisible': _write_off_states['invisible'],
})
2014-09-19 16:24:48 +02:00
@fields.depends('lines', 'currency', 'company')
def on_change_lines(self):
amount = Decimal(0)
2021-08-21 09:09:55 +02:00
if self.currency:
for line in self.lines:
if line.second_currency == self.currency:
amount += (
line.amount_second_currency or 0)
elif line.currency == self.currency:
amount += (
(line.debit or 0) - (line.credit or 0))
2021-08-21 09:09:55 +02:00
amount = self.currency.round(amount)
self.write_off_amount = amount
2008-03-19 22:13:51 +01:00
2014-05-09 23:32:17 +02:00
class CancelMoves(Wizard):
'Cancel Moves'
__name__ = 'account.move.cancel'
2014-11-10 23:35:54 +01:00
start_state = 'default'
default = StateView('account.move.cancel.default',
'account.move_cancel_default_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
2015-03-10 18:27:06 +01:00
Button('OK', 'cancel', 'tryton-ok', default=True),
2014-11-10 23:35:54 +01:00
])
2014-05-09 23:32:17 +02:00
cancel = StateTransition()
2014-11-10 23:35:54 +01:00
def default_cancel(self, move):
default = {}
if self.default.description:
default['description'] = self.default.description
return default
2014-05-09 23:32:17 +02:00
def transition_cancel(self):
pool = Pool()
Line = pool.get('account.move.line')
Move = pool.get('account.move')
2019-11-18 12:24:37 +01:00
Warning = pool.get('res.user.warning')
Unreconcile = pool.get('account.move.unreconcile_lines', type='wizard')
2014-05-09 23:32:17 +02:00
moves = self.records
2019-11-18 12:24:37 +01:00
moves_w_delegation = {
m: [ml for ml in m.lines
if ml.reconciliation and ml.reconciliation.delegate_to]
for m in moves}
if any(dml for dml in moves_w_delegation.values()):
names = ', '.join(m.rec_name for m in
islice(moves_w_delegation.keys(), None, 5))
if len(moves_w_delegation) > 5:
names += '...'
key = Warning.format('cancel_delegated', moves_w_delegation)
2019-11-18 12:24:37 +01:00
if Warning.check(key):
raise CancelDelegatedWarning(
key, gettext(
'account.msg_cancel_line_delegated', moves=names))
to_post = []
2014-05-09 23:32:17 +02:00
for move in moves:
2019-11-18 12:24:37 +01:00
if moves_w_delegation.get(move):
# Skip further warnings
with Transaction().set_user(0):
Unreconcile.make_unreconciliation(moves_w_delegation[move])
2014-11-10 23:35:54 +01:00
default = self.default_cancel(move)
2023-01-27 01:06:15 +01:00
cancel_move = move.cancel(
default=default, reversal=self.default.reversal)
if move.state == 'posted':
to_post.append(cancel_move)
2014-05-09 23:32:17 +02:00
to_reconcile = defaultdict(list)
for line in move.lines + cancel_move.lines:
if line.account.reconcile:
to_reconcile[(line.account, line.party)].append(line)
2018-06-26 19:11:40 +02:00
for lines in to_reconcile.values():
2014-05-09 23:32:17 +02:00
Line.reconcile(lines)
if to_post:
Move.post(to_post)
2014-05-09 23:32:17 +02:00
return 'end'
2014-11-10 23:35:54 +01:00
class CancelMovesDefault(ModelView):
'Cancel Moves'
__name__ = 'account.move.cancel.default'
description = fields.Char('Description')
2023-01-27 01:06:15 +01:00
reversal = fields.Boolean(
"Reversal",
help="Reverse debit and credit.")
@classmethod
def default_reversal(cls):
return True
2014-11-10 23:35:54 +01:00
2019-04-12 19:43:53 +02:00
class GroupLines(Wizard):
"Group Lines"
__name__ = 'account.move.line.group'
start = StateView('account.move.line.group.start',
'account.move_line_group_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Group", 'group', 'tryton-ok', default=True),
])
group = StateAction('account.act_move_form_grouping')
2019-04-12 19:43:53 +02:00
def do_group(self, action):
move, balance_line = self._group_lines(self.records)
if all(l.move.state == 'posted' for l in self.records):
move.post()
return action, {'res_id': move.id}
2019-11-25 12:08:39 +01:00
def _group_lines(self, lines, date=None):
move, balance_line = self.group_lines(lines, self.start.journal, date)
move.description = self.start.description
move.save()
2019-11-25 12:08:39 +01:00
return move, balance_line
@classmethod
def group_lines(cls, lines, journal, date=None):
pool = Pool()
Line = pool.get('account.move.line')
2019-11-25 12:08:39 +01:00
grouping = cls.grouping(lines)
move, balance_line = cls.get_move(lines, grouping, journal, date)
2019-04-12 19:43:53 +02:00
move.save()
to_reconcile = defaultdict(list)
for line in chain(lines, move.lines):
if line.account.reconcile:
to_reconcile[line.account].append(line)
if balance_line:
balance_line.move = move
balance_line.save()
2019-04-12 19:43:53 +02:00
for lines in to_reconcile.values():
Line.reconcile(lines, delegate_to=balance_line)
2019-11-25 12:08:39 +01:00
return move, balance_line
2019-04-12 19:43:53 +02:00
@classmethod
def grouping(cls, lines):
if len(lines) == 1:
raise GroupLineError(gettext('account.msg_group_line_single'))
companies = set()
parties = set()
accounts = set()
second_currencies = set()
for line in lines:
if not cls.allow_grouping(line.account):
raise GroupLineError(gettext('account.msg_group_line'))
companies.add(line.move.company)
parties.add(line.party)
accounts.add(line.account)
second_currencies.add(line.second_currency)
try:
company, = companies
except ValueError:
raise GroupLineError(
gettext('account.msg_group_line_same_company'))
try:
party, = parties
except ValueError:
raise GroupLineError(
gettext('account.msg_group_line_many_parties'))
try:
second_currency, = second_currencies
except ValueError:
raise GroupLineError(
gettext('account.msg_group_line_same_second_currency'))
if len(accounts) > 2:
raise GroupLineError(
gettext('account.msg_group_line_maximum_account'))
return {
'company': company,
'party': party,
'second_currency': second_currency,
'accounts': accounts,
}
@classmethod
def allow_grouping(cls, account):
return account.type.payable or account.type.receivable
2019-11-25 12:08:39 +01:00
@classmethod
def get_move(cls, lines, grouping, journal, date=None):
2019-04-12 19:43:53 +02:00
pool = Pool()
Date = pool.get('ir.date')
Move = pool.get('account.move')
Period = pool.get('account.period')
Line = pool.get('account.move.line')
2021-05-16 17:50:48 +02:00
company = grouping['company']
if not date:
with Transaction().set_context(company=company.id):
date = Date.today()
period = Period.find(company, date=date)
2019-04-12 19:43:53 +02:00
move = Move()
2021-05-16 17:50:48 +02:00
move.company = company
2019-04-12 19:43:53 +02:00
move.date = date
move.period = period
2019-11-25 12:08:39 +01:00
move.journal = journal
2019-04-12 19:43:53 +02:00
accounts = {a: 0 for a in grouping['accounts']}
amount_second_currency = 0
maturity_dates = {a: None for a in grouping['accounts']}
counterpart_lines = []
for line in lines:
if maturity_dates[line.account] and line.maturity_date:
2019-04-12 19:43:53 +02:00
maturity_dates[line.account] = min(
maturity_dates[line.account], line.maturity_date)
elif line.maturity_date:
2019-04-12 19:43:53 +02:00
maturity_dates[line.account] = line.maturity_date
2020-03-06 11:27:10 +01:00
cline = cls._counterpart_line(line)
accounts[cline.account] += cline.debit - cline.credit
if cline.amount_second_currency:
amount_second_currency += cline.amount_second_currency
counterpart_lines.append(cline)
2019-04-12 19:43:53 +02:00
move.lines = counterpart_lines
balance_line = None
if len(accounts) == 1:
account, = grouping['accounts']
amount = -accounts[account]
if amount:
balance_line = Line()
balance_line.account = account
else:
first, second = grouping['accounts']
if accounts[first] != accounts[second]:
balance_line = Line()
amount = -(accounts[first] + accounts[second])
if abs(accounts[first]) > abs(accounts[second]):
balance_line.account = first
else:
balance_line.account = second
if balance_line:
if balance_line.account.party_required:
balance_line.party = grouping['party']
if amount > 0:
balance_line.debit, balance_line.credit = amount, 0
else:
balance_line.debit, balance_line.credit = 0, -amount
if grouping['second_currency']:
balance_line.second_currency = grouping['second_currency']
balance_line.amount_second_currency = (
-amount_second_currency)
balance_line.maturity_date = maturity_dates[balance_line.account]
2019-04-12 19:43:53 +02:00
return move, balance_line
2019-11-25 12:08:39 +01:00
@classmethod
def _counterpart_line(cls, line):
2019-04-12 19:43:53 +02:00
pool = Pool()
Line = pool.get('account.move.line')
counterpart = Line()
counterpart.account = line.account
counterpart.debit = line.credit
counterpart.credit = line.debit
counterpart.party = line.party
counterpart.second_currency = line.second_currency
if line.second_currency:
counterpart.amount_second_currency = -line.amount_second_currency
else:
counterpart.amount_second_currency = None
return counterpart
class GroupLinesStart(ModelView):
"Group Lines"
__name__ = 'account.move.line.group.start'
journal = fields.Many2One('account.journal', "Journal", required=True)
description = fields.Char("Description")
2022-01-26 00:32:20 +01:00
class RescheduleLines(Wizard):
"Reschedule Lines"
__name__ = 'account.move.line.reschedule'
start = StateView('account.move.line.reschedule.start',
'account.move_line_reschedule_start_view_form', [
2021-08-20 23:45:02 +02:00
Button("Cancel", 'end', 'tryton-cancel'),
Button("Preview", 'preview', 'tryton-ok',
validate=False, default=True),
])
2022-01-26 00:32:20 +01:00
preview = StateView('account.move.line.reschedule.preview',
'account.move_line_reschedule_preview_view_form', [
2021-08-20 23:45:02 +02:00
Button("Cancel", 'end', 'tryton-cancel'),
2022-01-26 00:32:20 +01:00
Button("Reschedule", 'reschedule', 'tryton-ok', default=True),
2021-08-20 23:45:02 +02:00
])
2022-01-26 00:32:20 +01:00
reschedule = StateAction('account.act_move_form_rescheduling')
2021-08-20 23:45:02 +02:00
def get_origin(self):
try:
origin, = {r.move.origin for r in self.records}
except ValueError:
2022-01-26 00:32:20 +01:00
raise RescheduleLineError(
gettext('account.msg_reschedule_line_same_origins'))
2021-08-20 23:45:02 +02:00
return origin
@classmethod
def get_currency(cls, lines):
try:
currency, = {l.amount_currency for l in lines}
except ValueError:
2022-01-26 00:32:20 +01:00
raise RescheduleLineError(
gettext('account.msg_reschedule_line_same_currency'))
2021-08-20 23:45:02 +02:00
return currency
@classmethod
def get_account(cls, lines):
try:
account, = {l.account for l in lines}
except ValueError:
2022-01-26 00:32:20 +01:00
raise RescheduleLineError(
gettext('account.msg_reschedule_line_same_account'))
2021-08-20 23:45:02 +02:00
return account
@classmethod
def get_party(cls, lines):
try:
party, = {l.party for l in lines}
except ValueError:
2022-01-26 00:32:20 +01:00
raise RescheduleLineError(
gettext('account.msg_reschedule_line_same_party'))
2021-08-20 23:45:02 +02:00
return party
@classmethod
def get_total_amount(cls, lines):
return sum(l.amount for l in lines)
@classmethod
def get_balance(cls, lines):
return sum(l.debit - l.credit for l in lines)
def default_start(self, fields):
values = {}
self.get_origin()
self.get_account(self.records)
currency = self.get_currency(self.records)
values['currency'] = currency.id
values['total_amount'] = self.get_total_amount(self.records)
return values
def default_preview(self, fields):
values = {}
currency = self.get_currency(self.records)
try:
journal, = {r.move.journal for r in self.records}
values['journal'] = journal.id
except ValueError:
pass
values['currency'] = currency.id
if (self.start.start_date
and self.start.interval
and self.start.currency):
remaining = self.start.total_amount
date = self.start.start_date
values['terms'] = terms = []
if self.start.amount:
interval = self.start.interval
amount = self.start.amount.copy_sign(remaining)
while remaining - amount > 0:
2021-08-20 23:45:02 +02:00
terms.append({
'date': date,
'amount': amount,
2021-08-20 23:45:02 +02:00
'currency': currency.id,
})
date = (
self.start.start_date + relativedelta(months=interval))
interval += self.start.interval
remaining -= amount
2021-08-20 23:45:02 +02:00
if remaining:
terms.append({
'date': date,
'amount': remaining,
'currency': currency.id,
2021-08-20 23:45:02 +02:00
})
elif self.start.number:
amount = self.start.currency.round(
self.start.total_amount / self.start.number)
for i in range(self.start.number):
terms.append({
'date': date + relativedelta(months=i),
'amount': amount,
'currency': currency.id,
})
remaining -= amount
if remaining:
terms[-1]['amount'] += remaining
return values
2022-01-26 00:32:20 +01:00
def do_reschedule(self, action):
move, balance_line = self.reschedule_lines(
2021-08-20 23:45:02 +02:00
self.records, self.preview.journal, self.preview.terms)
move.origin = self.get_origin()
move.description = self.preview.description
move.save()
if all(l.move.state == 'posted' for l in self.records):
move.post()
2021-08-20 23:45:02 +02:00
action['res_id'] = [move.id]
return action, {}
@classmethod
def _line_values(cls, lines):
account = cls.get_account(lines)
currency = cls.get_currency(lines)
if currency == account.currency:
currency = None
return {
'account': account,
'second_currency': currency,
'party': cls.get_party(lines),
}
@classmethod
2022-01-26 00:32:20 +01:00
def reschedule_lines(cls, lines, journal, terms):
2021-08-20 23:45:02 +02:00
pool = Pool()
Lang = pool.get('ir.lang')
Line = pool.get('account.move.line')
total_amount = cls.get_total_amount(lines)
amount = sum(t.amount for t in terms)
if amount != total_amount:
lang = Lang.get()
currency = cls.get_currency(lines)
2022-01-26 00:32:20 +01:00
raise RescheduleLineError(
gettext('account.msg_reschedule_line_wrong_amount',
2021-08-20 23:45:02 +02:00
total_amount=lang.currency(total_amount, currency),
amount=lang.currency(amount, currency)))
balance = cls.get_balance(lines)
line_values = cls._line_values(lines)
account = line_values['account']
2022-01-26 00:32:20 +01:00
move, balance_line = cls.get_reschedule_move(
2021-08-20 23:45:02 +02:00
amount, balance, journal, terms, **line_values)
move.save()
balance_line.move = move
balance_line.save()
if account.reconcile:
Line.reconcile(lines + [balance_line])
return move, balance_line
@classmethod
2022-01-26 00:32:20 +01:00
def get_reschedule_move(
2021-08-20 23:45:02 +02:00
cls, amount, balance, journal, terms, account, date=None,
**line_values):
pool = Pool()
Date = pool.get('ir.date')
Line = pool.get('account.move.line')
Move = pool.get('account.move')
Period = pool.get('account.period')
company = account.company
if not date:
with Transaction().set_context(company=company.id):
date = Date.today()
period = Period.find(company, date=date)
2021-08-20 23:45:02 +02:00
move = Move()
move.company = company
move.date = date
move.period = period
move.journal = journal
balance_line = Line(account=account, **line_values)
if balance >= 0:
balance_line.debit, balance_line.credit = 0, balance
else:
balance_line.debit, balance_line.credit = -balance, 0
if balance_line.second_currency:
balance_line.amount_second_currency = -amount
remaining_balance = balance
remaining_amount = amount
lines = []
for term in terms:
line = Line(account=account, **line_values)
line.maturity_date = term.date
factor = term.amount / amount
line_amount = account.currency.round(balance * factor)
if balance >= 0:
line.debit, line.credit = line_amount, 0
else:
line.debit, line.credit = 0, -line_amount
remaining_balance -= line_amount
if line.second_currency:
line_amount_second_currency = line.second_currency.round(
amount * factor)
line.amount_second_currency = line_amount_second_currency
remaining_amount -= line_amount_second_currency
lines.append(line)
if remaining_balance:
if line.debit:
line.debit += remaining_balance
else:
line.credit += remaining_balance
if remaining_amount and line.second_currency:
line.amount_second_currency += remaining_amount
move.lines = lines
return move, balance_line
2022-01-26 00:32:20 +01:00
class RescheduleLinesStart(ModelView):
"Reschedule Lines"
__name__ = 'account.move.line.reschedule.start'
2021-08-20 23:45:02 +02:00
start_date = fields.Date("Start Date", required=True)
frequency = fields.Selection([
('monthly', "Monthly"),
('quarterly', "Quarterly"),
('other', "Other"),
], "Frequency", sort=False, required=True)
interval = fields.Integer(
"Interval", required=True,
states={
'invisible': Eval('frequency') != 'other',
},
help="The length of each period, in months.")
2021-08-21 09:09:55 +02:00
amount = Monetary(
"Amount", currency='currency', digits='currency',
2021-08-20 23:45:02 +02:00
states={
'required': ~Eval('number'),
'invisible': Bool(Eval('number')),
},
domain=[If(Eval('amount'),
If(Eval('total_amount', 0) > 0,
[
('amount', '<=', Eval('total_amount', 0)),
('amount', '>', 0),
],
[
('amount', '>=', Eval('total_amount', 0)),
('amount', '<', 0),
]),
[])])
2021-08-20 23:45:02 +02:00
number = fields.Integer(
"Number",
domain=[
('number', '>', 0),
],
states={
'required': ~Eval('amount'),
'invisible': Bool(Eval('amount')),
})
2021-08-20 23:45:02 +02:00
total_amount = fields.Numeric("Total Amount", readonly=True)
currency = fields.Many2One('currency.currency', "Currency", readonly=True)
@classmethod
def default_frequency(cls):
return 'monthly'
@classmethod
def frequency_intervals(cls):
return {
'monthly': 1,
'quarterly': 3,
'other': None,
}
@fields.depends('frequency', 'interval')
def on_change_frequency(self):
if self.frequency:
self.interval = self.frequency_intervals()[self.frequency]
2022-01-26 00:32:20 +01:00
class RescheduleLinesPreview(ModelView):
"Reschedule Lines"
__name__ = 'account.move.line.reschedule.preview'
2021-08-20 23:45:02 +02:00
journal = fields.Many2One('account.journal', "Journal", required=True)
description = fields.Char("Description")
terms = fields.One2Many(
2022-01-26 00:32:20 +01:00
'account.move.line.reschedule.term', None, "Terms",
2021-08-20 23:45:02 +02:00
domain=[
('currency', '=', Eval('currency', -1)),
])
2021-08-20 23:45:02 +02:00
currency = fields.Many2One('currency.currency', "Currency", readonly=True)
2022-01-26 00:32:20 +01:00
class RescheduleLinesTerm(ModelView):
"Reschedule Lines"
__name__ = 'account.move.line.reschedule.term'
2021-08-20 23:45:02 +02:00
date = fields.Date("Date", required=True)
2021-08-21 09:09:55 +02:00
amount = Monetary(
"Amount", currency='currency', digits='currency', required=True)
2021-08-20 23:45:02 +02:00
currency = fields.Many2One('currency.currency', "Currency", required=True)
class DelegateLines(Wizard):
"Delegate Lines"
__name__ = 'account.move.line.delegate'
start = StateView(
'account.move.line.delegate.start',
'account.move_line_delegate_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Delegate", 'delegate', 'tryton-ok', default=True),
])
delegate = StateAction('account.act_move_form_delegate')
def default_start(self, fields):
values = {}
if 'journal' in fields:
journals = {l.journal for l in self.records}
if len(journals) == 1:
journal, = journals
values['journal'] = journal.id
return values
def do_delegate(self, action):
move = self._delegate_lines(self.records, self.start.party)
if all(l.move.state == 'posted' for l in self.records):
move.post()
return action, {'res_id': move.id}
def _delegate_lines(self, lines, party, date=None):
move = self.delegate_lines(lines, party, self.start.journal, date)
move.description = self.start.description
move.save()
return move
@classmethod
def delegate_lines(cls, lines, party, journal, date=None):
pool = Pool()
Line = pool.get('account.move.line')
move, counterpart, delegated = cls.get_move(
lines, party, journal, date=date)
move.save()
Line.save(counterpart + delegated)
for line, cline, dline in zip(lines, counterpart, delegated):
Line.reconcile([line, cline], delegate_to=dline)
return move
@classmethod
def get_move(cls, lines, party, journal, date=None):
pool = Pool()
Date = pool.get('ir.date')
Move = pool.get('account.move')
Period = pool.get('account.period')
try:
company, = {l.company for l in lines}
except ValueError:
raise DelegateLineError(
gettext('account.msg_delegate_line_same_company'))
try:
origin, = {l.move.origin for l in lines}
except ValueError:
raise DelegateLineError(
gettext('account.msg_delegate_line_same_origins'))
if not date:
with Transaction().set_context(company=company.id):
date = Date.today()
period = Period.find(company, date=date)
move = Move()
move.company = company
move.date = date
move.period = period
move.journal = journal
move.origin = origin
counterpart = []
delegated = []
for line in lines:
cline = cls.get_move_line(line)
cline.move = move
cline.debit, cline.credit = line.credit, line.debit
if cline.amount_second_currency:
cline.amount_second_currency *= -1
counterpart.append(cline)
dline = cls.get_move_line(line)
dline.move = move
dline.party = party
delegated.append(dline)
return move, counterpart, delegated
@classmethod
def get_move_line(cls, line):
pool = Pool()
Line = pool.get('account.move.line')
new = Line()
new.debit = line.debit
new.credit = line.credit
new.account = line.account
new.origin = line
new.description = line.description
new.amount_second_currency = line.amount_second_currency
new.second_currency = line.second_currency
new.party = line.party
new.maturity_date = line.maturity_date
return new
class DelegateLinesStart(ModelView):
"Delegate Lines"
__name__ = 'account.move.line.delegate.start'
journal = fields.Many2One('account.journal', "Journal", required=True)
party = fields.Many2One('party.party', "Party", required=True)
description = fields.Char("Description")
2008-03-30 17:03:45 +02:00
class GeneralJournal(Report):
2012-09-24 22:17:29 +02:00
__name__ = 'account.move.general_journal'
2008-03-30 17:03:45 +02:00
2012-09-24 22:17:29 +02:00
@classmethod
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
2021-03-16 22:13:51 +01:00
pool = Pool()
Company = pool.get('company.company')
context = Transaction().context
2021-02-21 19:19:26 +01:00
report_context = super().get_context(records, header, data)
2021-03-16 22:13:51 +01:00
report_context['company'] = Company(context['company'])
2015-01-20 18:49:53 +01:00
return report_context