Files
tryton/move.py
T
2012-01-27 21:13:00 +01:00

1670 lines
65 KiB
Python

#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from decimal import Decimal
import datetime
import time
try:
import hashlib
except ImportError:
hashlib = None
import md5
from trytond.model import ModelView, ModelSQL, fields
from trytond.wizard import Wizard, StateTransition, StateView, StateAction, \
Button
from trytond.report import Report
from trytond.backend import TableHandler, FIELDS
from trytond.pyson import Eval, PYSONEncoder
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.config import CONFIG
_MOVE_STATES = {
'readonly': Eval('state') == 'posted',
}
_MOVE_DEPENDS = ['state']
_LINE_STATES = {
'readonly': Eval('state') == 'valid',
}
_LINE_DEPENDS = ['state']
class Move(ModelSQL, ModelView):
'Account Move'
_name = 'account.move'
_description = __doc__
name = fields.Char('Name', size=None, required=True)
reference = fields.Char('Reference', size=None, readonly=True,
help='Also known as Folio Number')
period = fields.Many2One('account.period', 'Period', required=True,
states=_MOVE_STATES, depends=_MOVE_DEPENDS, select=1)
journal = fields.Many2One('account.journal', 'Journal', required=True,
states=_MOVE_STATES, depends=_MOVE_DEPENDS)
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES,
depends=_MOVE_DEPENDS, on_change_with=['period', 'journal', 'date'])
post_date = fields.Date('Post Date', readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('posted', 'Posted'),
], 'State', required=True, readonly=True, select=1)
lines = fields.One2Many('account.move.line', 'move', 'Lines',
states=_MOVE_STATES, depends=_MOVE_DEPENDS,
context={
'journal': Eval('journal'),
'period': Eval('period'),
'date': Eval('date'),
})
def __init__(self):
super(Move, self).__init__()
self._constraints += [
('check_centralisation', 'period_centralized_journal'),
('check_company', 'company_in_move'),
('check_date', 'date_outside_period'),
]
self._rpc.update({
'button_post': True,
'button_draft': True,
})
self._order.insert(0, ('date', 'DESC'))
self._order.insert(1, ('reference', 'DESC'))
self._error_messages.update({
'del_posted_move': 'You can not delete posted moves!',
'post_empty_move': 'You can not post an empty move!',
'post_unbalanced_move': 'You can not post an unbalanced move!',
'modify_posted_move': 'You can not modify a posted move ' \
'in this journal!',
'period_centralized_journal': 'You can not create more than ' \
'one move per period\n' \
'in a centralized journal!',
'company_in_move': 'You can not create lines on accounts\n' \
'of different companies in the same move!',
'date_outside_period': 'You can not create move ' \
'with a date outside the period!',
})
def init(self, module_name):
super(Move, self).init(module_name)
cursor = Transaction().cursor
table = TableHandler(cursor, self, module_name)
table.index_action(['journal', 'period'], 'add')
# Add index on create_date
table.index_action('create_date', action='add')
def default_period(self):
period_obj = Pool().get('account.period')
return period_obj.find(Transaction().context.get('company') or False,
exception=False)
def default_state(self):
return 'draft'
def default_date(self):
period_obj = Pool().get('account.period')
date_obj = Pool().get('ir.date')
period_id = self.default_period()
if period_id:
period = period_obj.browse(period_id)
return period.start_date
return date_obj.today()
def on_change_with_date(self, vals):
line_obj = Pool().get('account.move.line')
period_obj = Pool().get('account.period')
res = vals['date']
line_ids = line_obj.search([
('journal', '=', vals.get('journal', False)),
('period', '=', vals.get('period', False)),
], order=[('id', 'DESC')], limit=1)
if line_ids:
line = line_obj.browse(line_ids[0])
res = line.date
elif vals.get('period'):
period = period_obj.browse(vals['period'])
res = period.start_date
return res
def check_centralisation(self, ids):
for move in self.browse(ids):
if move.journal.centralised:
move_ids = self.search([
('period', '=', move.period.id),
('journal', '=', move.journal.id),
('state', '!=', 'posted'),
], limit=2)
if len(move_ids) > 1:
return False
return True
def check_company(self, ids):
for move in self.browse(ids):
company_id = -1
for line in move.lines:
if company_id < 0:
company_id = line.account.company.id
if line.account.company.id != company_id:
return False
return True
def check_date(self, ids):
for move in self.browse(ids):
if move.date < move.period.start_date:
return False
if move.date > move.period.end_date:
return False
return True
def search_rec_name(self, name, clause):
ids = self.search(['OR',
('reference',) + clause[1:],
(self._rec_name,) + clause[1:],
])
return [('id', 'in', ids)]
def write(self, ids, vals):
res = super(Move, self).write(ids, vals)
self.validate(ids)
return res
def create(self, vals):
pool = Pool()
move_line_obj = pool.get('account.move.line')
sequence_obj = pool.get('ir.sequence')
journal_obj = pool.get('account.journal')
if not vals.get('name'):
journal_id = (vals.get('journal')
or Transaction().context.get('journal'))
if journal_id:
vals = vals.copy()
journal = journal_obj.browse(journal_id)
vals['name'] = sequence_obj.get_id(journal.sequence.id)
res = super(Move, self).create(vals)
move = self.browse(res)
if move.journal.centralised:
line_id = move_line_obj.create({
'account': move.journal.credit_account.id,
'move': move.id,
'name': 'Centralised Counterpart',
})
self.write(move.id, {
'centralised_line': line_id,
})
if 'lines' in vals:
self.validate([res])
return res
def delete(self, ids):
if isinstance(ids, (int, long)):
ids = [ids]
move_line_obj = Pool().get('account.move.line')
for move in self.browse(ids):
if move.state == 'posted':
self.raise_user_error('del_posted_move')
if move.lines:
move_line_ids = [x.id for x in move.lines]
move_line_obj.delete(move_line_ids)
return super(Move, self).delete(ids)
def copy(self, ids, default=None):
line_obj = Pool().get('account.move.line')
int_id = False
if isinstance(ids, (int, long)):
int_id = True
ids = [ids]
if default is None:
default = {}
default = default.copy()
default['reference'] = False
default['state'] = self.default_state()
default['post_date'] = False
default['lines'] = False
new_ids = []
for move in self.browse(ids):
new_id = super(Move, self).copy(move.id, default=default)
line_obj.copy([x.id for x in move.lines], default={
'move': new_id,
})
new_ids.append(new_id)
if int_id:
return new_ids[0]
return new_ids
def validate(self, ids):
'''
Validate balanced move and centralise it if in centralised journal
'''
currency_obj = Pool().get('currency.currency')
move_line_obj = Pool().get('account.move.line')
if isinstance(ids, (int, long)):
ids = [ids]
for move in self.browse(ids):
if not move.lines:
continue
amount = Decimal('0.0')
company = None
draft_lines = []
for line in move.lines:
amount += line.debit - line.credit
if not company:
company = line.account.company
if line.state == 'draft':
draft_lines.append(line)
if not currency_obj.is_zero(company.currency, amount):
if not move.journal.centralised:
move_line_obj.write(
[x.id for x in move.lines if x.state != 'draft'], {
'state': 'draft',
})
else:
if not move.centralised_line:
centralised_amount = - amount
else:
centralised_amount = move.centralised_line.debit \
- move.centralised_line.credit \
- amount
if centralised_amount >= Decimal('0.0'):
debit = centralised_amount
credit = Decimal('0.0')
account_id = move.journal.debit_account.id
else:
debit = Decimal('0.0')
credit = - centralised_amount
account_id = move.journal.credit_account.id
if not move.centralised_line:
centralised_line_id = move_line_obj.create({
'debit': debit,
'credit': credit,
'account': account_id,
'move': move.id,
'name': 'Centralised Counterpart',
})
self.write(move.id, {
'centralised_line': centralised_line_id,
})
else:
move_line_obj.write( move.centralised_line.id, {
'debit': debit,
'credit': credit,
'account': account_id,
})
continue
if not draft_lines:
continue
move_line_obj.write( [x.id for x in draft_lines], {
'state': 'valid',
})
return
def post(self, ids):
pool = Pool()
currency_obj = pool.get('currency.currency')
sequence_obj = pool.get('ir.sequence')
date_obj = pool.get('ir.date')
if isinstance(ids, (int, long)):
ids = [ids]
moves = self.browse(ids)
for move in moves:
amount = Decimal('0.0')
if not move.lines:
self.raise_user_error('post_empty_move')
company = None
for line in move.lines:
amount += line.debit - line.credit
if not company:
company = line.account.company
if not currency_obj.is_zero(company.currency, amount):
self.raise_user_error('post_unbalanced_move')
for move in moves:
reference = sequence_obj.get_id(move.period.post_move_sequence.id)
self.write(move.id, {
'reference': reference,
'state': 'posted',
'post_date': date_obj.today(),
})
return
def draft(self, ids):
for move in self.browse(ids):
if not move.journal.update_posted:
self.raise_user_error('modify_posted_move')
return self.write(ids, {
'state': 'draft',
})
def button_post(self, ids):
return self.post(ids)
def button_draft(self, ids):
return self.draft(ids)
Move()
class Reconciliation(ModelSQL, ModelView):
'Account Move Reconciliation Lines'
_name = 'account.move.reconciliation'
_description = __doc__
name = fields.Char('Name', size=None, required=True)
lines = fields.One2Many('account.move.line', 'reconciliation',
'Lines')
def __init__(self):
super(Reconciliation, self).__init__()
self._constraints += [
('check_lines', 'invalid_reconciliation'),
]
self._error_messages.update({
'modify': 'You can not modify a reconciliation!',
'invalid_reconciliation': 'You can not create reconciliation ' \
'where lines are not balanced, nor valid, ' \
'nor in the same account, nor in account to reconcile, ' \
'nor from the same party!',
})
def create(self, vals):
move_line_obj = Pool().get('account.move.line')
sequence_obj = Pool().get('ir.sequence')
if 'name' not in vals:
vals = vals.copy()
vals['name'] = sequence_obj.get('account.move.reconciliation')
res = super(Reconciliation, self).create(vals)
reconciliation = self.browse(res)
move_line_obj.workflow_trigger_trigger(
[x.id for x in reconciliation.lines])
return res
def write(self, ids, vals):
self.raise_user_error('modify')
def check_lines(self, ids):
currency_obj = Pool().get('currency.currency')
for reconciliation in self.browse(ids):
amount = Decimal('0.0')
account = None
party = None
for line in reconciliation.lines:
if line.state != 'valid':
return False
amount += line.debit - line.credit
if not account:
account = line.account
elif account.id != line.account.id:
return False
if not account.reconcile:
return False
if not party:
party = line.party
elif line.party and party.id != line.party.id:
return False
if not currency_obj.is_zero(account.company.currency, amount):
return False
return True
Reconciliation()
class Line(ModelSQL, ModelView):
'Account Move Line'
_name = 'account.move.line'
_description = __doc__
name = fields.Char('Name', size=None, required=True)
debit = fields.Numeric('Debit', digits=(16, Eval('currency_digits', 2)),
on_change=['account', 'debit', 'credit', 'tax_lines',
'journal', 'move'], depends=['currency_digits', 'credit',
'tax_lines', 'journal'])
credit = fields.Numeric('Credit', digits=(16, Eval('currency_digits', 2)),
on_change=['account', 'debit', 'credit', 'tax_lines',
'journal', 'move'], depends=['currency_digits', 'debit',
'tax_lines', 'journal'])
account = fields.Many2One('account.account', 'Account', required=True,
domain=[('kind', '!=', 'view')],
select=1,
on_change=['account', 'debit', 'credit', 'tax_lines',
'journal', 'move'])
move = fields.Many2One('account.move', 'Move', select=1, required=True,
states={
'required': False,
'readonly': Eval('state') == 'valid',
},
depends=['state'])
journal = fields.Function(fields.Many2One('account.journal', 'Journal'),
'get_move_field', setter='set_move_field',
searcher='search_move_field')
period = fields.Function(fields.Many2One('account.period', 'Period'),
'get_move_field', setter='set_move_field',
searcher='search_move_field')
date = fields.Function(fields.Date('Effective Date', required=True),
'get_move_field', setter='set_move_field',
searcher='search_move_field')
reference = fields.Char('Reference', size=None)
amount_second_currency = fields.Numeric('Amount Second Currency',
digits=(16, Eval('second_currency_digits', 2)),
help='The amount expressed in a second currency',
depends=['second_currency_digits'])
second_currency = fields.Many2One('currency.currency', 'Second Currency',
help='The second currency')
party = fields.Many2One('party.party', 'Party',
on_change=['move', 'party', 'account', 'debit', 'credit',
'journal'], select=1, depends=['debit', 'credit', 'account',
'journal'])
maturity_date = fields.Date('Maturity Date',
help='This field is used for payable and receivable lines. \n' \
'You can put the limit date for the payment.')
state = fields.Selection([
('draft', 'Draft'),
('valid', 'Valid'),
], 'State', readonly=True, required=True, select=1)
active = fields.Boolean('Active', select=2)
reconciliation = fields.Many2One('account.move.reconciliation',
'Reconciliation', readonly=True, ondelete='SET NULL', select=2)
tax_lines = fields.One2Many('account.tax.line', 'move_line', 'Tax Lines')
move_state = fields.Function(fields.Selection([
('draft', 'Draft'),
('posted', 'Posted'),
], 'Move State'), 'get_move_field', searcher='search_move_field')
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'get_currency_digits')
second_currency_digits = fields.Function(fields.Integer(
'Second Currency Digits'), 'get_currency_digits')
def __init__(self):
super(Line, self).__init__()
self._sql_constraints += [
('credit_debit',
'CHECK(credit * debit = 0.0)',
'Wrong credit/debit values!'),
]
self._constraints += [
('check_account', 'move_view_inactive_account'),
]
self._rpc.update({
'on_write': False,
})
self._order[0] = ('id', 'DESC')
self._error_messages.update({
'add_modify_closed_journal_period': 'You can not ' \
'add/modify lines in a closed journal period!',
'modify_posted_move': 'You can not modify lines of a posted move!',
'modify_reconciled': 'You can not modify reconciled lines!',
'no_journal': 'No journal defined!',
'move_view_inactive_account': 'You can not create move lines\n' \
'on view/inactive accounts!',
'already_reconciled': 'Line "%s" (%d) already reconciled!',
})
def init(self, module_name):
super(Line, self).init(module_name)
cursor = Transaction().cursor
table = TableHandler(cursor, self, module_name)
# Index for General Ledger
table.index_action(['move', 'account'], 'add')
# Migration from 1.2
table.not_null_action('blocked', action='remove')
def default_date(self):
'''
Return the date of the last line for journal, period
or the starting date of the period
or today
'''
period_obj = Pool().get('account.period')
date_obj = Pool().get('ir.date')
res = date_obj.today()
line_ids = self.search([
('journal', '=', Transaction().context.get('journal', False)),
('period', '=', Transaction().context.get('period', False)),
], order=[('id', 'DESC')], limit=1)
if line_ids:
line = self.browse(line_ids[0])
res = line.date
elif Transaction().context.get('period'):
period = period_obj.browse(Transaction().context['period'])
res = period.start_date
if Transaction().context.get('date'):
res = Transaction().context['date']
return res
def default_state(self):
return 'draft'
def default_active(self):
return True
def default_currency_digits(self):
return 2
def default_get(self, fields, with_rec_name=True):
pool = Pool()
move_obj = pool.get('account.move')
tax_obj = pool.get('account.tax')
account_obj = pool.get('account.account')
tax_code_obj = pool.get('account.tax.code')
currency_obj = pool.get('currency.currency')
values = super(Line, self).default_get(fields,
with_rec_name=with_rec_name)
if 'move' not in fields:
#Not manual entry
if 'date' in values:
values = values.copy()
del values['date']
return values
if (Transaction().context.get('journal')
and Transaction().context.get('period')):
line_ids = self.search([
('move.journal', '=', Transaction().context['journal']),
('move.period', '=', Transaction().context['period']),
('create_uid', '=', Transaction().user),
('state', '=', 'draft'),
], order=[('id', 'DESC')], limit=1)
if not line_ids:
return values
line = self.browse(line_ids[0])
values['move'] = line.move.id
if 'move' not in values:
return values
move = move_obj.browse(values['move'])
total = Decimal('0.0')
taxes = {}
no_code_taxes = []
for line in move.lines:
total += line.debit - line.credit
if line.party and 'party' in fields:
values.setdefault('party', line.party.id)
if 'reference' in fields:
values.setdefault('reference', line.reference)
if 'name' in fields:
values.setdefault('name', line.name)
if move.journal.type in ('expense', 'revenue'):
line_code_taxes = [x.code.id for x in line.tax_lines]
for tax in line.account.taxes:
if move.journal.type == 'revenue':
if line.debit:
base_id = tax.credit_note_base_code.id
code_id = tax.credit_note_tax_code.id
account_id = tax.credit_note_account.id
else:
base_id = tax.invoice_base_code.id
code_id = tax.invoice_tax_code.id
account_id = tax.invoice_account.id
else:
if line.debit:
base_id = tax.invoice_base_code.id
code_id = tax.invoice_tax_code.id
account_id = tax.invoice_account.id
else:
base_id = tax.credit_note_base_code.id
code_id = tax.credit_note_tax_code.id
account_id = tax.credit_note_account.id
if base_id in line_code_taxes or not base_id:
taxes.setdefault((account_id, code_id, tax.id), False)
for tax_line in line.tax_lines:
taxes[(line.account.id, tax_line.code.id,
tax_line.tax.id)] = True
if not line.tax_lines:
no_code_taxes.append(line.account.id)
for no_code_account_id in no_code_taxes:
for (account_id, code_id, tax_id), test in \
taxes.iteritems():
if (not test
and not code_id
and no_code_account_id == account_id):
taxes[(account_id, code_id, tax_id)] = True
if 'account' in fields:
if total >= Decimal('0.0'):
values.setdefault('account', move.journal.credit_account \
and move.journal.credit_account.id or False)
else:
values.setdefault('account', move.journal.debit_account \
and move.journal.debit_account.id or False)
if ('debit' in fields) or ('credit' in fields):
values.setdefault('debit', total < 0 and - total or False)
values.setdefault('credit', total > 0 and total or False)
if move.journal.type in ('expense', 'revenue'):
for account_id, code_id, tax_id in taxes:
if taxes[(account_id, code_id, tax_id)]:
continue
for line in move.lines:
if move.journal.type == 'revenue':
if line.debit:
key = 'credit_note'
else:
key = 'invoice'
else:
if line.debit:
key = 'invoice'
else:
key = 'credit_note'
line_amount = Decimal('0.0')
tax_amount = Decimal('0.0')
for tax_line in tax_obj.compute(
[x.id for x in line.account.taxes],
line.debit or line.credit, 1):
if (tax_line['tax'][key + '_account'].id \
or line.account.id) == account_id \
and tax_line['tax'][key + '_tax_code'].id \
== code_id \
and tax_line['tax'].id == tax_id:
if line.debit:
line_amount += tax_line['amount']
else:
line_amount -= tax_line['amount']
tax_amount += tax_line['amount'] * \
tax_line['tax'][key + '_tax_sign']
line_amount = currency_obj.round(
line.account.company.currency, line_amount)
tax_amount = currency_obj.round(
line.account.company.currency, tax_amount)
if ('debit' in fields):
values['debit'] = line_amount > Decimal('0.0') \
and line_amount or Decimal('0.0')
if ('credit' in fields):
values['credit'] = line_amount < Decimal('0.0') \
and - line_amount or Decimal('0.0')
if 'account' in fields:
values['account'] = account_id
values['account.rec_name'] = account_obj.browse(
account_id).rec_name
if 'tax_lines' in fields and code_id:
values['tax_lines'] = [
{
'amount': tax_amount,
'currency_digits': line.currency_digits,
'code': code_id,
'code.rec_name': tax_code_obj.browse(
code_id).rec_name,
'tax': tax_id,
'tax.rec_name': tax_obj.browse(tax_id).rec_name
},
]
return values
def get_currency_digits(self, ids, names):
res = {}
for line in self.browse(ids):
for name in names:
res.setdefault(name, {})
res[name].setdefault(line.id, 2)
if name == 'currency_digits':
res[name][line.id] = line.account.currency_digits
elif name == 'second_currency_digits':
if line.account.second_currency:
res[name][line.id] = line.account.second_currency.digits
return res
def on_change_debit(self, vals):
res = {}
journal_obj = Pool().get('account.journal')
if vals.get('journal') or Transaction().context.get('journal'):
journal = journal_obj.browse(vals.get('journal')
or Transaction().context.get('journal'))
if journal.type in ('expense', 'revenue'):
res['tax_lines'] = self._compute_tax_lines(vals, journal.type)
if not res['tax_lines']:
del res['tax_lines']
if vals.get('debit'):
res['credit'] = Decimal('0.0')
return res
def on_change_credit(self, vals):
res = {}
journal_obj = Pool().get('account.journal')
if vals.get('journal') or Transaction().context.get('journal'):
journal = journal_obj.browse(vals.get('journal')
or Transaction().context.get('journal'))
if journal.type in ('expense', 'revenue'):
res['tax_lines'] = self._compute_tax_lines(
vals, journal.type)
if not res['tax_lines']:
del res['tax_lines']
if vals.get('credit'):
res['debit'] = Decimal('0.0')
return res
def on_change_account(self, vals):
account_obj = Pool().get('account.account')
journal_obj = Pool().get('account.journal')
res = {}
if Transaction().context.get('journal'):
journal = journal_obj.browse(Transaction().context['journal'])
if journal.type in ('expense', 'revenue'):
res['tax_lines'] = self._compute_tax_lines(vals, journal.type)
if not res['tax_lines']:
del res['tax_lines']
if vals.get('account'):
account = account_obj.browse(vals['account'])
res['currency_digits'] = account.currency_digits
if account.second_currency:
res['second_currency_digits'] = account.second_currency.digits
return res
def _compute_tax_lines(self, vals, journal_type):
res = {}
pool = Pool()
account_obj = pool.get('account.account')
tax_code_obj = pool.get('account.tax.code')
tax_obj = pool.get('account.tax')
if vals.get('move'):
#Only for first line
return res
if vals.get('tax_lines'):
res['remove'] = [x['id'] for x in vals['tax_lines']]
if vals.get('account'):
account = account_obj.browse(vals['account'])
debit = vals.get('debit', Decimal('0.0'))
credit = vals.get('credit', Decimal('0.0'))
for tax in account.taxes:
if journal_type == 'revenue':
if debit:
key = 'credit_note'
else:
key = 'invoice'
else:
if debit:
key = 'invoice'
else:
key = 'credit_note'
base_amounts = {}
for tax_line in tax_obj.compute(
[x.id for x in account.taxes],
debit or credit, 1):
code_id = tax_line['tax'][key + '_base_code'].id
if not code_id:
continue
tax_id = tax_line['tax'].id
base_amounts.setdefault((code_id, tax_id), Decimal('0.0'))
base_amounts[code_id, tax_id] += tax_line['base'] * \
tax_line['tax'][key + '_tax_sign']
for code_id, tax_id in base_amounts:
if not base_amounts[code_id, tax_id]:
continue
res.setdefault('add', []).append({
'amount': base_amounts[code_id, tax_id],
'currency_digits': account.currency_digits,
'code': code_id,
'code.rec_name': tax_code_obj.browse(code_id).rec_name,
'tax': tax_id,
'tax.rec_name': tax_obj.browse(tax_id).rec_name,
})
return res
def on_change_party(self, vals):
pool = Pool()
party_obj = pool.get('party.party')
journal_obj = pool.get('account.journal')
currency_obj = pool.get('currency.currency')
cursor = Transaction().cursor
res = {}
if (not vals.get('party')) or vals.get('account'):
return res
party = party_obj.browse(vals.get('party'))
if not party.account_receivable \
or not party.account_payable:
return res
if party and (not vals.get('debit')) and (not vals.get('credit')):
type_name = FIELDS[self.debit._type].sql_type(self.debit)[0]
query = 'SELECT ' \
'CAST(COALESCE(SUM(' \
'(COALESCE(debit, 0) - COALESCE(credit, 0))' \
'), 0) AS ' + type_name + ') ' \
'FROM account_move_line ' \
'WHERE reconciliation IS NULL ' \
'AND party = %s ' \
'AND account = %s'
cursor.execute(query, (party.id, party.account_receivable.id))
amount = cursor.fetchone()[0]
# SQLite uses float for SUM
if not isinstance(amount, Decimal):
amount = Decimal(str(amount))
if not currency_obj.is_zero( party.account_receivable.currency,
amount):
if amount > Decimal('0.0'):
res['credit'] = currency_obj.round(
party.account_receivable.currency, amount)
res['debit'] = Decimal('0.0')
else:
res['credit'] = Decimal('0.0')
res['debit'] = - currency_obj.round(
party.account_receivable.currency, amount)
res['account'] = party.account_receivable.id
res['account.rec_name'] = party.account_receivable.rec_name
else:
cursor.execute(query, (party.id, party.account_payable.id))
amount = cursor.fetchone()[0]
if not currency_obj.is_zero( party.account_payable.currency,
amount):
if amount > Decimal('0.0'):
res['credit'] = currency_obj.round(
party.account_payable.currency, amount)
res['debit'] = Decimal('0.0')
else:
res['credit'] = Decimal('0.0')
res['debit'] = - currency_obj.round(
party.account_payable.currency, amount)
res['account'] = party.account_payable.id
res['account.rec_name'] = party.account_payable.rec_name
if party and vals.get('debit'):
if vals['debit'] > Decimal('0.0'):
if 'account' not in res:
res['account'] = party.account_receivable.id
res['account.rec_name'] = party.account_receivable.rec_name
else:
if 'account' not in res:
res['account'] = party.account_payable.id
res['account.rec_name'] = party.account_payable.id
if party and vals.get('credit'):
if vals['credit'] > Decimal('0.0'):
if 'account' not in res:
res['account'] = party.account_payable.id
res['account.rec_name'] = party.account_payable.rec_name
else:
if 'account' not in res:
res['account'] = party.account_receivable.id
res['account.rec_name'] = party.account_receivable.rec_name
journal_id = (vals.get('journal')
or Transaction().context.get('journal'))
if journal_id and party:
journal = journal_obj.browse(journal_id)
if journal.type == 'revenue':
if 'account' not in res:
res['account'] = party.account_receivable.id
res['account.rec_name'] = party.account_receivable.rec_name
elif journal.type == 'expense':
if 'account' not in res:
res['account'] = party.account_payable.id
res['account.rec_name'] = party.account_payable.rec_name
return res
def get_move_field(self, ids, name):
if name == 'move_state':
name = 'state'
if name not in ('period', 'journal', 'date', 'state'):
raise Exception('Invalid name')
res = {}
for line in self.browse(ids):
if name in ('date', 'state'):
res[line.id] = line.move[name]
else:
res[line.id] = line.move[name].id
return res
def set_move_field(self, ids, name, value):
if name == 'move_state':
name = 'state'
if name not in ('period', 'journal', 'date', 'state'):
raise Exception('Invalid name')
if not value:
return
move_obj = Pool().get('account.move')
lines = self.browse(ids)
move_obj.write([line.move.id for line in lines], {
name: value,
})
def search_move_field(self, name, clause):
if name == 'move_state':
name = 'state'
return [('move.' + name,) + tuple(clause[1:])]
def query_get(self, obj='l'):
'''
Return SQL clause and fiscal years for account move line
depending of the context.
:param obj: the SQL alias of account_move_line in the query
:return: a tuple with the SQL clause and the fiscalyear ids
'''
fiscalyear_obj = Pool().get('account.fiscalyear')
if Transaction().context.get('date'):
time.strptime(str(Transaction().context['date']), '%Y-%m-%d')
fiscalyear_ids = fiscalyear_obj.search([
('start_date', '<=', Transaction().context['date']),
('end_date', '>=', Transaction().context['date']),
], limit=1)
fiscalyear_id = fiscalyear_ids and fiscalyear_ids[0] or 0
if Transaction().context.get('posted'):
return (obj + '.active ' \
'AND ' + obj + '.state != \'draft\' ' \
'AND ' + obj + '.move IN (' \
'SELECT m.id FROM account_move AS m, ' \
'account_period AS p ' \
'WHERE m.period = p.id ' \
'AND p.fiscalyear = ' + \
str(fiscalyear_id) + ' ' \
'AND m.date <= date(\'' + \
str(Transaction().context['date']) + \
'\') ' \
'AND m.state = \'posted\' ' \
')', fiscalyear_ids)
else:
return (obj + '.active ' \
'AND ' + obj + '.state != \'draft\' ' \
'AND ' + obj + '.move IN (' \
'SELECT m.id FROM account_move AS m, ' \
'account_period AS p ' \
'WHERE m.period = p.id ' \
'AND p.fiscalyear = ' + \
str(fiscalyear_id) + ' ' \
'AND m.date <= date(\'' + \
str(Transaction().context['date']) + \
'\')' \
')', fiscalyear_ids)
if not Transaction().context.get('fiscalyear'):
fiscalyear_ids = fiscalyear_obj.search([
('state', '=', 'open'),
])
fiscalyear_clause = (','.join(map(str, fiscalyear_ids))) or '0'
else:
fiscalyear_ids = [int(Transaction().context.get('fiscalyear'))]
fiscalyear_clause = '%s' % int(
Transaction().context.get('fiscalyear'))
if Transaction().context.get('periods'):
ids = ','.join(
str(int(x)) for x in Transaction().context['periods'])
if Transaction().context.get('posted'):
return (obj + '.active ' \
'AND ' + obj + '.state != \'draft\' ' \
'AND ' + obj + '.move IN (' \
'SELECT id FROM account_move ' \
'WHERE period IN (' + ids + ') ' \
'AND state = \'posted\' ' \
')', [])
else:
return (obj + '.active ' \
'AND ' + obj + '.state != \'draft\' ' \
'AND ' + obj + '.move IN (' \
'SELECT id FROM account_move ' \
'WHERE period IN (' + ids + ')' \
')', [])
else:
if Transaction().context.get('posted'):
return (obj + '.active ' \
'AND ' + obj + '.state != \'draft\' ' \
'AND ' + obj + '.move IN (' \
'SELECT id FROM account_move ' \
'WHERE period IN (' \
'SELECT id FROM account_period ' \
'WHERE fiscalyear IN (' + fiscalyear_clause + ')' \
') ' \
'AND state = \'posted\' ' \
')', fiscalyear_ids)
else:
return (obj + '.active ' \
'AND ' + obj + '.state != \'draft\' ' \
'AND ' + obj + '.move IN (' \
'SELECT id FROM account_move ' \
'WHERE period IN (' \
'SELECT id FROM account_period ' \
'WHERE fiscalyear IN (' + fiscalyear_clause + ')' \
')' \
')', fiscalyear_ids)
def on_write(self, ids):
lines = self.browse(ids)
res = []
for line in lines:
res.extend([x.id for x in line.move.lines])
return list({}.fromkeys(res))
def check_account(self, ids):
for line in self.browse(ids):
if line.account.kind in ('view',):
return False
if not line.account.active:
return False
return True
def check_journal_period_modify(self, period_id, journal_id):
'''
Check if the lines can be modified or created for the journal - period
and if there is no journal - period, create it
'''
pool = Pool()
journal_period_obj = pool.get('account.journal.period')
journal_obj = pool.get('account.journal')
period_obj = pool.get('account.period')
journal_period_ids = journal_period_obj.search([
('journal', '=', journal_id),
('period', '=', period_id),
], limit=1)
if journal_period_ids:
journal_period = journal_period_obj.browse(journal_period_ids[0])
if journal_period.state == 'close':
self.raise_user_error('add_modify_closed_journal_period')
else:
journal = journal_obj.browse(journal_id)
period = period_obj.browse(period_id)
journal_period_obj.create({
'name': journal.name + ' - ' + period.name,
'journal': journal.id,
'period': period.id,
})
return
def check_modify(self, ids):
'''
Check if the lines can be modified
'''
journal_period_done = []
for line in self.browse(ids):
if line.move.state == 'posted':
self.raise_user_error('modify_posted_move')
if line.reconciliation:
self.raise_user_error('modify_reconciled')
journal_period = (line.journal.id, line.period.id)
if journal_period not in journal_period_done:
self.check_journal_period_modify(line.period.id,
line.journal.id)
journal_period_done.append(journal_period)
return
def delete(self, ids):
move_obj = Pool().get('account.move')
if isinstance(ids, (int, long)):
ids = [ids]
self.check_modify(ids)
lines = self.browse(ids)
move_ids = [x.move.id for x in lines]
res = super(Line, self).delete(ids)
move_obj.validate(move_ids)
return res
def write(self, ids, vals):
move_obj = Pool().get('account.move')
if isinstance(ids, (int, long)):
ids = [ids]
if len(vals) > 1 or 'reconciliation' not in vals:
self.check_modify(ids)
lines = self.browse(ids)
move_ids = [x.move.id for x in lines]
res = super(Line, self).write(ids, vals)
Transaction().timestamp = {}
lines = self.browse(ids)
for line in lines:
if line.move.id not in move_ids:
move_ids.append(line.move.id)
move_obj.validate(move_ids)
return res
def create(self, vals):
journal_obj = Pool().get('account.journal')
move_obj = Pool().get('account.move')
vals = vals.copy()
if not vals.get('move'):
journal_id = (vals.get('journal')
or Transaction().context.get('journal'))
if not journal_id:
self.raise_user_error('no_journal')
journal = journal_obj.browse(journal_id)
if journal.centralised:
move_ids = move_obj.search([
('period', '=', vals.get('period')
or Transaction().context.get('period')),
('journal', '=', journal_id),
('state', '!=', 'posted'),
], limit=1)
if move_ids:
vals['move'] = move_ids[0]
if not vals.get('move'):
vals['move'] = move_obj.create({
'period': (vals.get('period')
or Transaction().context.get('period')),
'journal': journal_id,
'date': vals.get('date', False),
})
res = super(Line, self).create(vals)
line = self.browse(res)
self.check_journal_period_modify(line.period.id, line.journal.id)
move_obj.validate([vals['move']])
return res
def copy(self, ids, default=None):
if default is None:
default = {}
if 'move' not in default:
default['move'] = False
if 'reconciliation' not in default:
default['reconciliation'] = False
return super(Line, self).copy(ids, default=default)
def view_header_get(self, value, view_type='form'):
journal_period_obj = Pool().get('account.journal.period')
if (not Transaction().context.get('journal')
or not Transaction().context.get('period')):
return value
journal_period_ids = journal_period_obj.search([
('journal', '=', Transaction().context['journal']),
('period', '=', Transaction().context['period']),
], limit=1)
if not journal_period_ids:
return value
journal_period = journal_period_obj.browse(journal_period_ids[0])
return value + ': ' + journal_period.rec_name
def fields_view_get(self, view_id=None, view_type='form', hexmd5=None):
journal_obj = Pool().get('account.journal')
result = super(Line, self).fields_view_get(view_id=view_id,
view_type=view_type, hexmd5=hexmd5)
if view_type == 'tree' and 'journal' in Transaction().context:
title = self.view_header_get('', view_type=view_type)
journal = journal_obj.browse(Transaction().context['journal'])
if not journal.view:
return result
xml = '<?xml version="1.0"?>\n' \
'<tree string="%s" editable="top" on_write="on_write" ' \
'colors="red:state==\'draft\'">\n' % title
fields = set()
for column in journal.view.columns:
fields.add(column.field.name)
attrs = []
if column.field.name == 'debit':
attrs.append('sum="Debit"')
elif column.field.name == 'credit':
attrs.append('sum="Credit"')
if column.readonly:
attrs.append('readonly="1"')
if column.required:
attrs.append('required="1"')
else:
attrs.append('required="0"')
xml += '<field name="%s" %s/>\n' % (column.field.name, ' '.join(attrs))
for depend in getattr(self, column.field.name).depends:
fields.add(depend)
fields.add('state')
xml += '</tree>'
result['arch'] = xml
result['fields'] = self.fields_get(fields_names=list(fields))
del result['md5']
if hashlib:
result['md5'] = hashlib.md5(str(result)).hexdigest()
else:
result['md5'] = md5.new(str(result)).hexdigest()
if hexmd5 == result['md5']:
return True
return result
def reconcile(self, ids, journal_id=False, date=False, account_id=False):
pool = Pool()
move_obj = pool.get('account.move')
currency_obj = pool.get('currency.currency')
reconciliation_obj = pool.get('account.move.reconciliation')
period_obj = pool.get('account.period')
date_obj = pool.get('ir.date')
translation_obj = pool.get('ir.translation')
for line in self.browse(ids):
if line.reconciliation:
self.raise_user_error('already_reconciled',
error_args=(line.name, line.id,))
ids = ids[:]
if journal_id and account_id:
if not date:
date = date_obj.today()
account = None
amount = Decimal('0.0')
for line in self.browse(ids):
amount += line.debit - line.credit
if not account:
account = line.account
amount = currency_obj.round(account.currency, amount)
period_id = period_obj.find(account.company.id, date=date)
lang_code = CONFIG['language']
if account.company.lang:
lang_code = account.company.lang.code
writeoff = translation_obj.get_source(
'account.move.reconcile_lines.writeoff', 'view', lang_code,
'Write-Off') or 'Write-Off'
move_id = move_obj.create({
'journal': journal_id,
'period': period_id,
'date': date,
'lines': [
('create', {
'name': writeoff,
'account': account.id,
'debit': amount < Decimal('0.0') and - amount \
or Decimal('0.0'),
'credit': amount > Decimal('0.0') and amount \
or Decimal('0.0'),
}),
('create', {
'name': writeoff,
'account': account_id,
'debit': amount > Decimal('0.0') and amount \
or Decimal('0.0'),
'credit': amount < Decimal('0.0') and - amount \
or Decimal('0.0'),
}),
],
})
ids += self.search([
('move', '=', move_id),
('account', '=', account.id),
('debit', '=', amount < Decimal('0.0') and - amount \
or Decimal('0.0')),
('credit', '=', amount > Decimal('0.0') and amount \
or Decimal('0.0')),
], limit=1)
return reconciliation_obj.create({
'lines': [('add', x) for x in ids],
})
Line()
class Move2(ModelSQL, ModelView):
_name = 'account.move'
centralised_line = fields.Many2One('account.move.line', 'Centralised Line',
readonly=True)
Move2()
class OpenJournalAsk(ModelView):
'Open Journal Ask'
_name = 'account.move.open_journal.ask'
_description = __doc__
journal = fields.Many2One('account.journal', 'Journal', required=True)
period = fields.Many2One('account.period', 'Period', required=True,
domain=[
('state', '!=', 'close'),
('fiscalyear.company.id', '=',
Eval('context', {}).get('company', 0)),
])
def default_period(self):
period_obj = Pool().get('account.period')
return period_obj.find(Transaction().context.get('company') or False,
exception=False)
OpenJournalAsk()
class OpenJournal(Wizard):
'Open Journal'
_name = 'account.move.open_journal'
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')
def transition_start(self, session):
if (Transaction().context.get('active_model', '') == 'account.journal.period'
and Transaction().context.get('active_id')):
return 'open_'
return 'ask'
def default_ask(self, session, fields):
journal_period_obj = Pool().get('account.journal.period')
if (Transaction().context.get('active_model', '') ==
'account.journal.period'
and Transaction().context.get('active_id')):
journal_period = \
journal_period_obj.browse(Transaction().context['active_id'])
return {
'journal': journal_period.journal.id,
'period': journal_period.period.id,
}
return {}
def do_open_(self, session, action):
pool = Pool()
journal_period_obj = pool.get('account.journal.period')
journal_obj = pool.get('account.journal')
period_obj = pool.get('account.period')
if (Transaction().context.get('active_model', '') ==
'account.journal.period'
and Transaction().context.get('active_id')):
journal_period = \
journal_period_obj.browse(Transaction().context['active_id'])
journal_id = journal_period.journal.id
period_id = journal_period.period.id
else:
journal_id = session.ask.journal.id
period_id = session.ask.period.id
journal_period_ids = journal_period_obj.search([
('journal', '=', journal_id),
('period', '=', period_id),
])
if not journal_period_ids:
journal = journal_obj.browse(journal_id)
period = period_obj.browse(period_id)
journal_period_id = journal_period_obj.create({
'name': journal.name + ' - ' + period.name,
'journal': journal.id,
'period': period.id,
})
else:
journal_period_id = journal_period_ids[0]
journal_period = journal_period_obj.browse(journal_period_id)
action['name'] += ' - %s' % journal_period.rec_name
action['pyson_domain'] = PYSONEncoder().encode([
('journal', '=', journal_id),
('period', '=', period_id),
])
action['pyson_context'] = PYSONEncoder().encode({
'journal': journal_id,
'period': period_id,
})
return action, {}
def transition_open_(self, session):
return 'end'
OpenJournal()
class OpenAccount(Wizard):
'Open Account'
_name = 'account.move.open_account'
start_state = 'open_'
open_ = StateAction('account.act_move_line_form')
def do_open_(self, session, action):
pool = Pool()
fiscalyear_obj = pool.get('account.fiscalyear')
if not Transaction().context.get('fiscalyear'):
fiscalyear_ids = fiscalyear_obj.search([
('state', '=', 'open'),
])
else:
fiscalyear_ids = [Transaction().context['fiscalyear']]
period_ids = []
for fiscalyear in fiscalyear_obj.browse(fiscalyear_ids):
for period in fiscalyear.periods:
period_ids.append(period.id)
action['pyson_domain'] = [
('period', 'in', period_ids),
('account', '=', Transaction().context['active_id']),
]
if Transaction().context.get('posted'):
action['pyson_domain'].append(('move.state', '=', 'posted'))
action['pyson_domain'] = PYSONEncoder().encode(action['pyson_domain'])
action['pyson_context'] = PYSONEncoder().encode({
'fiscalyear': Transaction().context.get('fiscalyear'),
})
return action, {}
OpenAccount()
class ReconcileLinesWriteOff(ModelView):
'Reconcile Lines Write-Off'
_name = 'account.move.reconcile_lines.writeoff'
_description = __doc__
journal = fields.Many2One('account.journal', 'Journal', required=True)
date = fields.Date('Date', required=True)
account = fields.Many2One('account.account', 'Account', required=True,
domain=[('kind', '!=', 'view')])
def default_date(self):
date_obj = Pool().get('ir.date')
return date_obj.today()
ReconcileLinesWriteOff()
class ReconcileLines(Wizard):
'Reconcile Lines'
_name = 'account.move.reconcile_lines'
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()
def transition_start(self, session):
line_obj = Pool().get('account.move.line')
currency_obj = Pool().get('currency.currency')
company = None
amount = Decimal('0.0')
for line in line_obj.browse(Transaction().context['active_ids']):
amount += line.debit - line.credit
if not company:
company = line.account.company
if not company:
return 'end'
if currency_obj.is_zero(company.currency, amount):
return 'reconcile'
return 'writeoff'
def transiton_reconcile(self, session):
line_obj = Pool().get('account.move.line')
journal_id = False
date = False
account_id = False
if session.writeoff.journal:
journal_id = session.writeoff.journal.id
if session.writeoff.date:
date = session.writeoff.date
if session.writeoff.account:
account_id = session.writeoff.account.id
line_obj.reconcile(Transaction().context['active_ids'], journal_id,
date, account_id)
return 'end'
ReconcileLines()
class UnreconcileLinesStart(ModelView):
'Unreconcile Lines'
_name = 'account.move.unreconcile_lines.start'
_description = __doc__
UnreconcileLinesStart()
class UnreconcileLines(Wizard):
'Unreconcile Lines'
_name = 'account.move.unreconcile_lines'
start = StateView('account.move.unreconcile_lines.start',
'account.unreconcile_lines_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Unreconcile', 'unreconcile', 'tryton-ok', default=True),
])
unreconcile = StateTransition()
def transition_unreconcile(self, session):
line_obj = Pool().get('account.move.line')
reconciliation_obj = Pool().get('account.move.reconciliation')
lines = line_obj.browse(Transaction().context['active_ids'])
reconciliation_ids = [x.reconciliation.id for x in lines \
if x.reconciliation]
if reconciliation_ids:
reconciliation_obj.delete(reconciliation_ids)
return 'end'
UnreconcileLines()
class OpenReconcileLinesStart(ModelView):
'Open Reconcile Lines'
_name = 'account.move.open_reconcile_lines.start'
_description = __doc__
account = fields.Many2One('account.account', 'Account', required=True,
domain=[('kind', '!=', 'view'), ('reconcile', '=', True)])
OpenReconcileLinesStart()
class OpenReconcileLines(Wizard):
'Open Reconcile Lines'
_name = 'account.move.open_reconcile_lines'
start = StateView('account.move.open_reconcile_lines.start',
'account.open_reconcile_lines_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'open_', 'tryton-ok', default=True),
])
open_ = StateAction('account.act_move_line_form')
def do_open_(self, session, action):
action['pyson_domain'] = PYSONEncoder().encode([
('account', '=', session.start.account.id),
('reconciliation', '=', False),
])
return action, {}
OpenReconcileLines()
class FiscalYearLine(ModelSQL):
'Fiscal Year - Move Line'
_name = 'account.fiscalyear-account.move.line'
_table = 'account_fiscalyear_line_rel'
_description = __doc__
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year',
ondelete='CASCADE', select=1)
line = fields.Many2One('account.move.line', 'Line', ondelete='RESTRICT',
select=1, required=True)
FiscalYearLine()
class FiscalYear(ModelSQL, ModelView):
_name = 'account.fiscalyear'
close_lines = fields.Many2Many('account.fiscalyear-account.move.line',
'fiscalyear', 'line', 'Close Lines')
FiscalYear()
class PrintGeneralJournalStart(ModelView):
'Print General Journal'
_name = 'account.move.print_general_journal.start'
_description = __doc__
from_date = fields.Date('From Date', required=True)
to_date = fields.Date('To Date', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
posted = fields.Boolean('Posted Move', help='Show only posted move')
def default_from_date(self):
date_obj = Pool().get('ir.date')
return datetime.date(date_obj.today().year, 1, 1)
def default_to_date(self):
date_obj = Pool().get('ir.date')
return date_obj.today()
def default_company(self):
return Transaction().context.get('company') or False
def default_posted(self):
return False
PrintGeneralJournalStart()
class PrintGeneralJournal(Wizard):
'Print General Journal'
_name = 'account.move.print_general_journal'
start = StateView('account.move.print_general_journal.start',
'account.print_general_journal_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-print', default=True),
])
print_ = StateAction('account.report_general_journal')
def do_print_(self, session, action):
data = {
'company': session.start.company.id,
'from_date': session.start.from_date,
'to_date': session.start.to_date,
'posted': session.start.posted,
}
return action, data
PrintGeneralJournal()
class GeneralJournal(Report):
_name = 'account.move.general_journal'
def _get_objects(self, ids, model, data):
move_obj = Pool().get('account.move')
clause = [
('date', '>=', data['from_date']),
('date', '<=', data['to_date']),
]
if data['posted']:
clause.append(('state', '=', 'posted'))
move_ids = move_obj.search(clause,
order=[('date', 'ASC'), ('reference', 'ASC'), ('id', 'ASC')])
return move_obj.browse(move_ids)
def parse(self, report, objects, data, localcontext):
company_obj = Pool().get('company.company')
company = company_obj.browse(data['company'])
localcontext['company'] = company
localcontext['digits'] = company.currency.digits
localcontext['from_date'] = data['from_date']
localcontext['to_date'] = data['to_date']
return super(GeneralJournal, self).parse(report, objects, data,
localcontext)
GeneralJournal()