From 439d7a5a9083aea9f81b1c626f9699be15f705e6 Mon Sep 17 00:00:00 2001 From: bch <> Date: Thu, 4 Jan 2007 09:45:50 +0000 Subject: [PATCH] ACCOUNT: * bank statment line now have a ref t othe corresponding invoice. * account.py splitted for depedendy reason. L10N_CH: debug bzr revid: bch-cd59941716bff05a29b95739b303b6f1c2a859a8 --- addons/account/__init__.py | 3 + addons/account/account.py | 636 ++--------------------- addons/account/account_analytic_line.py | 130 +++++ addons/account/account_bank_statement.py | 192 +++++++ addons/account/account_invoice_view.xml | 5 +- addons/account/account_move_line.py | 466 +++++++++++++++++ addons/account/account_view.xml | 3 +- addons/account/invoice.py | 36 +- addons/account/project/project.py | 94 ---- addons/l10n_ch/dta/dta_wizard.py | 43 +- 10 files changed, 882 insertions(+), 726 deletions(-) create mode 100644 addons/account/account_analytic_line.py create mode 100644 addons/account/account_bank_statement.py create mode 100644 addons/account/account_move_line.py diff --git a/addons/account/__init__.py b/addons/account/__init__.py index b62474360b1..43169f60587 100644 --- a/addons/account/__init__.py +++ b/addons/account/__init__.py @@ -29,6 +29,9 @@ import account import project import invoice +import account_bank_statement +import account_move_line +import account_analytic_line import transfer import wizard import report diff --git a/addons/account/account.py b/addons/account/account.py index c2c0476d7cb..45fc3eb5376 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -70,6 +70,31 @@ class account_payment_term(osv.osv): result.append( (next_date.strftime('%Y-%m-%d'), amt) ) amount -= amt return result + + def get_discounts(self,cr,uid,id,base_date, context={}): + """ + return the list of (date,percentage) ordered by date for the + payment term with the corresponding id. return [] if no cash + discount are defined. base_date is the date from where the + discounts are computed. + """ + + pt = self.browse(cr, uid, id, context) + + if not pt.cash_discount_ids: + return [] + + res=[] + for d in pt.cash_discount_ids: + res.append( + ((mx.DateTime.strptime(base_date,'%Y-%m-%d') +\ + RelativeDateTime(days=d.delay+1)).strftime("%Y-%m-%d"), + d.discount) + ) + + res.sort(cmp=lambda x,y: cmp(x[0],y[0])) + return res + account_payment_term() class account_payment_term_line(osv.osv): @@ -93,18 +118,6 @@ class account_payment_term_line(osv.osv): account_payment_term_line() -class account_cash_discount(osv.osv): - _name = "account.cash.discount" - _description = "Cash Discount" #A reduction in the price if payment is made within a stipulated period. - _columns = { - 'name': fields.char('Name', size=32), - 'delay': fields.integer('Number of Days', required=True), - 'discount': fields.float('Discount (%)',required=True), - 'payment_id': fields.many2one('account.payment.term','Associated Payment Term'), - } -account_cash_discount() - - class account_account_type(osv.osv): _name = "account.account.type" _description = "Account Type" @@ -248,6 +261,21 @@ class account_account(osv.osv): return res account_account() + +class account_cash_discount(osv.osv): + _name = "account.cash.discount" + _description = "Cash Discount" #A reduction in the price if payment is made within a stipulated period. + _columns = { + 'name': fields.char('Name', size=32), + 'delay': fields.integer('Number of Days', required=True), + 'discount': fields.float('Discount (%)',digits=(16,6),required=True), + 'payment_id': fields.many2one('account.payment.term','Associated Payment Term'), + 'credit_account_id': fields.many2one('account.account', 'Credit Account'), + 'debit_account_id': fields.many2one('account.account', 'Debit Account'), + } +account_cash_discount() + + class account_journal_view(osv.osv): _name = "account.journal.view" _description = "Journal View" @@ -698,157 +726,6 @@ class account_move_reconcile(osv.osv): } account_move_reconcile() -# -# use a sequence for names ? -# -class account_bank_statement(osv.osv): - def _default_journal_id(self, cr, uid, context={}): - if context.get('journal_id', False): - return context['journal_id'] - return False - - def _default_balance_start(self, cr, uid, context={}): - cr.execute('select id from account_bank_statement where journal_id=%d order by date desc limit 1', (1,)) - res = cr.fetchone() - if res: - return self.browse(cr, uid, [res[0]], context)[0].balance_end - return 0.0 - - def _end_balance(self, cr, uid, ids, prop, unknow_none, unknow_dict): - res = {} - statements = self.browse(cr, uid, ids) - for statement in statements: - res[statement.id] = statement.balance_start - for line in statement.line_ids: - res[statement.id] += line.amount - for r in res: - res[r] = round(res[r], 2) - return res - - def _get_period(self, cr, uid, context={}): - periods = self.pool.get('account.period').find(cr, uid) - if periods: - return periods[0] - else: - return False - - _order = "date desc" - _name = "account.bank.statement" - _description = "Bank Statement" - _columns = { - 'name': fields.char('Name', size=64, required=True), - 'date': fields.date('Date', required=True, states={'confirm':[('readonly',True)]}), - 'journal_id': fields.many2one('account.journal', 'Journal', required=True, states={'confirm':[('readonly',True)]}, domain=[('type','=','cash')], relate=True), - 'period_id': fields.many2one('account.period', 'Period', required=True, states={'confirm':[('readonly',True)]}), - 'balance_start': fields.float('Starting Balance', digits=(16,2), states={'confirm':[('readonly',True)]}), - 'balance_end_real': fields.float('Ending Balance', digits=(16,2), states={'confirm':[('readonly',True)]}), - 'balance_end': fields.function(_end_balance, method=True, string='Balance'), - 'line_ids': fields.one2many('account.bank.statement.line', 'statement_id', 'Statement lines', states={'confirm':[('readonly',True)]}), - 'move_line_ids': fields.one2many('account.move.line', 'statement_id', 'Entry lines', states={'confirm':[('readonly',True)]}), - 'state': fields.selection([('draft','Draft'),('confirm','Confirm')], 'State', required=True, states={'confirm':[('readonly',True)]}, readonly="1"), - #'partner_id': fields.many2one('res.partner', 'Partner', states={'confirm':[('readonly',True)]}), - #'invoice_id': fields.many2one('account.invoice', 'Invoice', states={'confirm':[('readonly',True)]}), - #write-off account - #selection: partiel/total - } - - _defaults = { - 'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement'), - 'date': lambda *a: time.strftime('%Y-%m-%d'), - 'state': lambda *a: 'draft', - 'balance_start': _default_balance_start, - 'journal_id': _default_journal_id, - 'period_id': _get_period, - } - def button_confirm(self, cr, uid, ids, context={}): - done = [] - for st in self.browse(cr, uid, ids, context): - if not st.state=='draft': - continue - if not (abs(st.balance_end - st.balance_end_real) < 0.0001): - raise osv.except_osv('Error !', 'The statement balance is incorrect !\nCheck that the ending balance equals the computed one.') - if (not st.journal_id.default_credit_account_id) or (not st.journal_id.default_debit_account_id): - raise osv.except_osv('Configration Error !', 'Please verify that an account is defined in the journal.') - for move in st.line_ids: - if not move.amount: - continue - self.pool.get('account.move.line').create(cr, uid, { - 'name': move.name, - 'date': move.date, - 'partner_id': ((move.partner_id) and move.partner_id.id) or False, - 'account_id': (move.account_id) and move.account_id.id, - 'credit': ((move.amount>0) and move.amount) or 0.0, - 'debit': ((move.amount<0) and -move.amount) or 0.0, - 'statement_id': st.id, - 'journal_id': st.journal_id.id, - 'period_id': st.period_id.id, - }, context=context) - if not st.journal_id.centralisation: - c = context.copy() - c['journal_id'] = st.journal_id.id - c['period_id'] = st.period_id.id - fields = ['move_id','name','date','partner_id','account_id','credit','debit'] - default = self.pool.get('account.move.line').default_get(cr, uid, fields, context=c) - default.update({ - 'statement_id': st.id, - 'journal_id': st.journal_id.id, - 'period_id': st.period_id.id, - }) - self.pool.get('account.move.line').create(cr, uid, default, context=context) - done.append(st.id) - self.write(cr, uid, done, {'state':'confirm'}, context=context) - return True - def button_cancel(self, cr, uid, ids, context={}): - done = [] - for st in self.browse(cr, uid, ids, context): - if st.state=='draft': - continue - ids = [x.move_id.id for x in st.move_line_ids] - self.pool.get('account.move').unlink(cr, uid, ids, context) - done.append(st.id) - self.write(cr, uid, done, {'state':'draft'}, context=context) - return True - def onchange_journal_id(self, cr, uid, id, journal_id, context={}): - if not journal_id: - return {} - cr.execute('select balance_end_real from account_bank_statement where journal_id=%d order by date desc limit 1', (journal_id,)) - res = cr.fetchone() - if res: - return {'value': {'balance_start': res[0] or 0.0}} - return {} -account_bank_statement() - -class account_bank_statement_line(osv.osv): - def onchange_partner_id(self, cr, uid, id, partner_id, type, context={}): - if not partner_id: - return {} - part = self.pool.get('res.partner').browse(cr, uid, partner_id, context) - if type=='supplier': - account_id = part.property_account_payable[0] - else: - account_id = part.property_account_receivable[0] - cr.execute('select sum(debit-credit) from account_move_line where (reconcile_id is null) and partner_id=%d and account_id=%d', (partner_id, account_id)) - balance = cr.fetchone()[0] or 0.0 - val = {'amount': balance, 'account_id':account_id} - return {'value':val} - _order = "date,name desc" - _name = "account.bank.statement.line" - _description = "Bank Statement Line" - _columns = { - 'name': fields.char('Name', size=64, required=True), - 'date': fields.date('Date'), - 'amount': fields.float('Amount'), - 'type': fields.selection([('supplier','Supplier'),('customer','Customer'),('general','General')], 'Type', required=True), - 'partner_id': fields.many2one('res.partner', 'Partner'), - 'account_id': fields.many2one('account.account','Account', required=True), - 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True), - } - _defaults = { - 'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement.line'), - 'date': lambda *a: time.strftime('%Y-%m-%d'), - 'type': lambda *a: 'general', - } -account_bank_statement_line() #---------------------------------------------------------- @@ -903,439 +780,6 @@ class account_tax_code(osv.osv): } account_tax_code() -class account_move_line(osv.osv): - _name = "account.move.line" - _description = "Entry lines" - - def default_get(self, cr, uid, fields, context={}): - data = self._default_get(cr, uid, fields, context) - for f in data.keys(): - if f not in fields: - del data[f] - return data - - def _default_get(self, cr, uid, fields, context={}): - # Compute simple values - data = super(account_move_line, self).default_get(cr, uid, fields, context) - - # Compute the current move - move_id = False - partner_id = False - statement_acc_id = False - if context.get('journal_id',False) and context.get('period_id',False): - cr.execute('select move_id \ - from \ - account_move_line \ - where \ - journal_id=%d and period_id=%d and create_uid=%d and state=%s \ - order by id desc limit 1', (context['journal_id'], context['period_id'], uid, 'draft')) - res = cr.fetchone() - move_id = (res and res[0]) or False - cr.execute('select date \ - from \ - account_move_line \ - where \ - journal_id=%d and period_id=%d and create_uid=%d order by id desc', (context['journal_id'], context['period_id'], uid)) - res = cr.fetchone() - data['date'] = res and res[0] or time.strftime('%Y-%m-%d') - cr.execute('select statement_id, account_id \ - from \ - account_move_line \ - where \ - journal_id=%d and period_id=%d and statement_id is not null and create_uid=%d order by id desc', (context['journal_id'], context['period_id'], uid)) - res = cr.fetchone() - statement_id = res and res[0] or False - statement_acc_id = res and res[1] - - if not move_id: - return data - - data['move_id'] = move_id - - total = 0 - taxes = {} - move = self.pool.get('account.move').browse(cr, uid, move_id, context) - for l in move.line_id: - partner_id = partner_id or l.partner_id.id - total += (l.debit - l.credit) - for tax in l.account_id.tax_ids: - acc = (l.debit >0) and tax.account_paid_id.id or tax.account_collected_id.id - taxes.setdefault((acc,tax.tax_code_id.id), False) - taxes[(l.account_id.id,l.tax_code_id.id)] = True - data.setdefault('name', l.name) - - data['partner_id'] = partner_id - - print taxes - for t in taxes: - if not taxes[t] and t[0]: - s=0 - for l in move.line_id: - for tax in l.account_id.tax_ids: - taxes = self.pool.get('account.tax').compute(cr, uid, [tax.id], l.debit or l.credit, 1, False) - key = (l.debit and 'account_paid_id') or 'account_collected_id' - for t2 in taxes: - if (t2[key] == t[0]) and (tax.tax_code_id.id==t[1]): - if l.debit: - s += t2['amount'] - else: - s -= t2['amount'] - data['debit'] = s>0 and s or 0.0 - data['credit'] = s<0 and -s or 0.0 - - data['tax_code_id'] = t[1] - - data['account_id'] = t[0] - - # - # Compute line for tax T - # - return data - - # - # Compute latest line - # - data['credit'] = total>0 and total - data['debit'] = total<0 and -total - if total>=0: - data['account_id'] = move.journal_id.default_credit_account_id.id or False - else: - data['account_id'] = move.journal_id.default_debit_account_id.id or False - if data['account_id']: - account = self.pool.get('account.account').browse(cr, uid, data['account_id']) - data['tax_code_id'] = self._default_get_tax(cr, uid, account ) - return data - - def _default_get_tax(self, cr, uid, account, debit=0, credit=0, context={}): - if account.tax_ids: - return account.tax_ids[0].base_code_id.id - return False - - def _on_create_write(self, cr, uid, id, context={}): - ml = self.browse(cr, uid, id, context) - return map(lambda x: x.id, ml.move_id.line_id) - - def _balance(self, cr, uid, ids, prop, unknow_none, unknow_dict): - res={} - # TODO group the foreach in sql - for id in ids: - cr.execute('SELECT date,account_id FROM account_move_line WHERE id=%d', (id,)) - dt, acc = cr.fetchone() - cr.execute('SELECT SUM(debit-credit) FROM account_move_line WHERE account_id=%d AND (date<%s OR (date=%s AND id<=%d)) and active', (acc,dt,dt,id)) - res[id] = cr.fetchone()[0] - return res - - _columns = { - 'name': fields.char('Name', size=64, required=True), - 'quantity': fields.float('Quantity', digits=(16,2), help="The optionnal quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very usefull for some reports."), - 'debit': fields.float('Debit', digits=(16,2), states={'reconciled':[('readonly',True)]}), - 'credit': fields.float('Credit', digits=(16,2), states={'reconciled':[('readonly',True)]}), - 'account_id': fields.many2one('account.account', 'Account', required=True, ondelete="cascade", states={'reconciled':[('readonly',True)]}, domain=[('type','<>','view')]), - - 'move_id': fields.many2one('account.move', 'Entry', required=True, ondelete="cascade", states={'reconciled':[('readonly',True)]}, help="The entry of this entry line.", select=True), - - 'ref': fields.char('Ref.', size=32), - 'statement_id': fields.many2one('account.bank.statement', 'Statement', help="The bank statement used for bank reconciliation", select=True), - 'reconcile_id': fields.many2one('account.move.reconcile', 'Reconcile', readonly=True, ondelete='set null', select=True), - 'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optionnal other currency if it is a multi-currency entry."), - 'currency_id': fields.many2one('res.currency', 'Currency', help="The optionnal other currency if it is a multi-currency entry."), - - 'period_id': fields.many2one('account.period', 'Period', required=True), - 'journal_id': fields.many2one('account.journal', 'Journal', required=True, relate=True), - 'blocked': fields.boolean('Litigation', help="You can check this box to mark the entry line as a litigation with the associated partner"), - - 'partner_id': fields.many2one('res.partner', 'Partner Ref.', states={'reconciled':[('readonly',True)]}), - 'date_maturity': fields.date('Maturity date', states={'reconciled':[('readonly',True)]}, help="This field is used for payable and receivable entries. You can put the limit date for the payment of this entry line."), - 'date': fields.date('Effective date', required=True), - 'date_created': fields.date('Creation date'), - 'analytic_lines': fields.one2many('account.analytic.line', 'move_id', 'Analytic lines'), - 'centralisation': fields.selection([('normal','Normal'),('credit','Credit Centralisation'),('debit','Debit Centralisation')], 'Centralisation', size=6), - 'balance': fields.function(_balance, method=True, string='Balance'), - 'active': fields.boolean('Active'), - 'state': fields.selection([('draft','Draft'), ('valid','Valid'), ('reconciled','Reconciled')], 'State', readonly=True), - 'tax_code_id': fields.many2one('account.tax.code', 'Tax Account'), - 'tax_amount': fields.float('Tax/Base Amount', digits=(16,2), select=True), - } - _defaults = { - 'blocked': lambda *a: False, - 'active': lambda *a: True, - 'centralisation': lambda *a: 'normal', - 'date_created': lambda *a: time.strftime('%Y-%m-%d'), - 'state': lambda *a: 'draft', - 'journal_id': lambda self, cr, uid, c: c.get('journal_id', False), - 'period_id': lambda self, cr, uid, c: c.get('period_id', False), - } - _order = "date desc,id desc" - _sql_constraints = [ - ('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'), - ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'), - ] - def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, journal=False): - if (not partner_id) or account_id: - return {} - part = self.pool.get('res.partner').browse(cr, uid, partner_id) - id1 = part.property_account_payable[0] - id2 = part.property_account_receivable[0] - cr.execute('select sum(debit-credit) from account_move_line where (reconcile_id is null) and partner_id=%d and account_id=%d', (partner_id, id2)) - balance = cr.fetchone()[0] or 0.0 - val = {} - if (not debit) and (not credit): - if abs(balance)>0.01: - val['credit'] = ((balance>0) and balance) or 0 - val['debit'] = ((balance<0) and -balance) or 0 - val['account_id'] = id2 - else: - cr.execute('select sum(debit-credit) from account_move_line where (reconcile_id is null) and partner_id=%d and account_id=%d', (partner_id, id1)) - balance = cr.fetchone()[0] or 0.0 - val['credit'] = ((balance>0) and balance) or 0 - val['debit'] = ((balance<0) and -balance) or 0 - val['account_id'] = id1 - else: - val['account_id'] = (debit>0) and id2 or id1 - if journal: - jt = self.pool.get('account.journal').browse(cr, uid, journal).type - if jt=='sale': - val['account_id'] = id2 - elif jt=='purchase': - val['account_id'] = id1 - return {'value':val} - - # - # type: the type if reconciliation (no logic behind this field, for infà) - # - # writeoff; entry generated for the difference between the lines - # - - def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context={}): - id_set = ','.join(map(str, ids)) - lines = self.read(cr, uid, ids, context=context) - unrec_lines = filter(lambda x: not x['reconcile_id'], lines) - credit = debit = 0 - account_id = False - partner_id = False - for line in unrec_lines: - credit += line['credit'] - debit += line['debit'] - account_id = line['account_id'][0] - partner_id = (line['partner_id'] and line['partner_id'][0]) or False - writeoff = debit - credit - date = time.strftime('%Y-%m-%d') - - cr.execute('SELECT account_id,reconcile_id FROM account_move_line WHERE id IN ('+id_set+') GROUP BY account_id,reconcile_id') - r = cr.fetchall() -#TODO: move this check to a constraint in the account_move_reconcile object - if len(r) != 1: - raise Exception('Entries are not of the same account or already reconciled ! ') - if r[0][1] != None: - raise Exception('Some entries are already reconciled !') - if writeoff != 0: - if not writeoff_acc_id: - raise osv.except_osv('Warning', 'You have to provide an account for the write off entry !') - if writeoff > 0: - debit = writeoff - credit = 0.0 - self_credit = writeoff - self_debit = 0.0 - else: - debit = 0.0 - credit = -writeoff - self_credit = 0.0 - self_debit = -writeoff - - writeoff_lines = [ - (0, 0, {'name':'Write-Off', 'debit':self_debit, 'credit':self_credit, 'account_id':account_id, 'date':date, 'partner_id':partner_id}), - (0, 0, {'name':'Write-Off', 'debit':debit, 'credit':credit, 'account_id':writeoff_acc_id, 'date':date, 'partner_id':partner_id}) - ] - - name = 'Write-Off' - if writeoff_journal_id: - journal = self.pool.get('account.journal').browse(cr, uid, writeoff_journal_id) - if journal.sequence_id: - name = self.pool.get('ir.sequence').get_id(cr, uid, journal.sequence_id.id) - - writeoff_move_id = self.pool.get('account.move').create(cr, uid, { - 'name': name, - 'period_id': writeoff_period_id, - 'journal_id': writeoff_journal_id, - - 'state': 'draft', - 'line_id': writeoff_lines - }) - - writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)]) - ids += writeoff_line_ids - - self.write(cr, uid, ids, {'state': 'reconciled'}, update_check=False) - r_id = self.pool.get('account.move.reconcile').create(cr, uid, { - 'name': date, - 'type': type, - 'line_id': map(lambda x: (4,x,False), ids) - }) - # the id of the move.reconcile is written in the move.line (self) by the create method above - # because of the way the line_id are defined: (4, x, False) - wf_service = netsvc.LocalService("workflow") - for id in ids: - wf_service.trg_trigger(uid, 'account.move.line', id, cr) - return r_id - - def view_header_get(self, cr, user, view_id, view_type, context): - if (not context.get('journal_id', False)) or (not context.get('period_id', False)): - return False - cr.execute('select code from account_journal where id=%d', (context['journal_id'],)) - j = cr.fetchone()[0] or '' - cr.execute('select code from account_period where id=%d', (context['period_id'],)) - p = cr.fetchone()[0] or '' - if j or p: - return j+':'+p - return 'Journal' - - def fields_view_get(self, cr, uid, view_id=None, view_type='form', context={}, toolbar=False): - result = super(osv.osv, self).fields_view_get(cr, uid, view_id,view_type,context) - if view_type=='tree' and 'journal_id' in context: - title = self.view_header_get(cr, uid, view_id, view_type, context) - journal = self.pool.get('account.journal').browse(cr, uid, context['journal_id']) - - # if the journal view has a state field, color lines depending on - # its value - state = '' - for field in journal.view_id.columns_id: - if field.field=='state': - state = ' colors="red:state==\'draft\'"' - - #xml = '''\n\n\t''' % (title, state) - xml = '''\n\n\t''' % (title, state) - fields = [] - - widths = { - 'ref': 50, - 'statement_id': 50, - 'state': 60, - 'tax_code_id': 50, - 'move_id': 40, - } - for field in journal.view_id.columns_id: - fields.append(field.field) - attrs = [] - if field.readonly: - attrs.append('readonly="1"') - if field.required: - attrs.append('required="1"') - else: - attrs.append('required="0"') - if field.field == 'partner_id': - attrs.append('on_change="onchange_partner_id(move_id,partner_id,account_id,debit,credit,((\'journal_id\' in context) and context[\'journal_id\']) or {})"') - if field.field in widths: - attrs.append('width="'+str(widths[field.field])+'"') - xml += '''\n''' % (field.field,' '.join(attrs)) - - xml += '''''' - result['arch'] = xml - result['fields'] = self.fields_get(cr, uid, fields, context) - return result - - def unlink(self, cr, uid, ids, context={}, check=True): - self._update_check(cr, uid, ids, context) - for line in self.browse(cr, uid, ids, context): - context['journal_id']=line.journal_id.id - context['period_id']=line.period_id.id - result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context) - if check: - self.pool.get('account.move').validate(cr, uid, [line.move_id.id], context=context) - return result - - # - # TO VERIFY: check if try to write journal of only one line ??? - # - def write(self, cr, uid, ids, vals, context={}, check=True, update_check=True): - if update_check: - self._update_check(cr, uid, ids, context) - result = super(osv.osv, self).write(cr, uid, ids, vals, context) - if check: - done = [] - for line in self.browse(cr, uid, ids): - if line.move_id.id not in done: - done.append(line.move_id.id) - self.pool.get('account.move').validate(cr, uid, [line.move_id.id], context) - return result - - def _update_journal_check(self, cr, uid, journal_id, period_id, context={}): - cr.execute('select state from account_journal_period where journal_id=%d and period_id=%d', (journal_id, period_id)) - result = cr.fetchall() - for (state,) in result: - if state=='done': - raise osv.except_osv('Error !', 'You can not add/modify entries in a closed journal.') - if not result: - journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context) - period = self.pool.get('account.period').browse(cr, uid, period_id, context) - self.pool.get('account.journal.period').create(cr, uid, { - 'name': (journal.code or journal.name)+':'+(period.name or ''), - 'journal_id': journal.id, - 'period_id': period.id - }) - return True - - def _update_check(self, cr, uid, ids, context={}): - done = {} - for line in self.browse(cr, uid, ids, context): - if line.move_id.state<>'draft': - raise osv.except_osv('Error !', 'You can not modify or delete a confirmed entry !') - if line.reconcile_id: - raise osv.except_osv('Error !', 'You can not modify or delete a reconciled entry !') - t = (line.journal_id.id, line.period_id.id) - if t not in done: - self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context) - done[t] = True - return True - - def create(self, cr, uid, vals, context={}, check=True): - if 'journal_id' in vals and 'journal_id' not in context: - context['journal_id'] = vals['journal_id'] - if 'period_id' in vals and 'period_id' not in context: - context['period_id'] = vals['period_id'] - if 'journal_id' not in context and 'move_id' in vals: - m = self.pool.get('account.move').browse(cr, uid, vals['move_id']) - context['journal_id'] = m.journal_id.id - context['period_id'] = m.period_id.id - self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context) - move_id = vals.get('move_id', False) - journal = self.pool.get('account.journal').browse(cr, uid, context['journal_id']) - if not move_id: - if journal.centralisation: - # use the first move ever created for this journal and period - cr.execute('select id from account_move where journal_id=%d and period_id=%d order by id limit 1', (context['journal_id'],context['period_id'])) - res = cr.fetchone() - if res: - vals['move_id'] = res[0] - - if not vals.get('move_id', False): - if journal.sequence_id: - name = self.pool.get('ir.sequence').get_id(cr, uid, journal.sequence_id.id) - v = { - 'name': name, - 'period_id': context['period_id'], - 'journal_id': context['journal_id'] - } - move_id = self.pool.get('account.move').create(cr, uid, v, context) - vals['move_id'] = move_id - else: - raise osv.except_osv('No piece number !', 'Can not create an automatic sequence for this piece !\n\nPut a sequence in the journal definition for automatic numbering or create a sequence manually for this piece.') - - if ('account_id' in vals) and journal.type_control_ids: - type = self.pool.get('account.account').browse(cr, uid, vals['account_id']).type - ok = False - for t in journal.type_control_ids: - if type==t.code: - ok = True - break - if not ok: - raise osv.except_osv('Bad account !', 'You can not use this general account in this journal !') - - result = super(osv.osv, self).create(cr, uid, vals, context) - if check: - self.pool.get('account.move').validate(cr, uid, [vals['move_id']], context) - return result -account_move_line() - class account_tax(osv.osv): """ A tax object. diff --git a/addons/account/account_analytic_line.py b/addons/account/account_analytic_line.py new file mode 100644 index 00000000000..77e605c1d46 --- /dev/null +++ b/addons/account/account_analytic_line.py @@ -0,0 +1,130 @@ + +############################################################################## +# +# Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved. +# +# $Id$ +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +import time + +from osv import fields +from osv import osv + +class account_analytic_line(osv.osv): + _name = 'account.analytic.line' + _columns = { + 'name' : fields.char('Description', size=128, required=True), + 'date' : fields.date('Date', required=True), + 'amount' : fields.float('Amount', required=True), + 'unit_amount' : fields.float('Quantity'), + 'product_uom_id' : fields.many2one('product.uom', 'UoM'), + 'product_id' : fields.many2one('product.product', 'Product'), + 'account_id' : fields.many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='cascade', select=True), + 'general_account_id' : fields.many2one('account.account', 'General account', required=True, ondelete='cascade'), + 'move_id' : fields.many2one('account.move.line', 'General entry', ondelete='cascade', select=True), + 'journal_id' : fields.many2one('account.analytic.journal', 'Analytic journal', required=True, ondelete='cascade', select=True), + 'code' : fields.char('Code', size=8), + 'user_id' : fields.many2one('res.users', 'User',), + } + + _defaults = { + 'date': lambda *a: time.strftime('%Y-%m-%d'), + } + _order = 'date' + + def on_change_unit_amount(self, cr, uid, id, prod_id, unit_amount, unit=False, context={}): + if unit_amount and prod_id: + rate = 1 + if unit: + uom_id = self.pool.get('product.uom') + hunit = uom_id.browse(cr, uid, unit) + rate = hunit.factor + uom_id = self.pool.get('product.product') + prod = uom_id.browse(cr, uid, prod_id) + a = prod.product_tmpl_id.property_account_expense + if not a: + a = prod.categ_id.property_account_expense_categ + return {'value' : {'amount' : -round(unit_amount * prod.standard_price * rate,2), 'general_account_id':a[0]}} + return {} + +account_analytic_line() + + +class timesheet_invoice(osv.osv): + _name = "report.hr.timesheet.invoice.journal" + _description = "Analytic account costs and revenues" + _auto = False + _columns = { + 'name': fields.date('Month', readonly=True), + 'account_id':fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, relate=True, select=True), + 'journal_id': fields.many2one('account.analytic.journal', 'Journal', readonly=True), + 'quantity': fields.float('Quantities', readonly=True), + 'cost': fields.float('Credit', readonly=True), + 'revenue': fields.float('Debit', readonly=True) + } + _order = 'name desc, account_id' + def init(self, cr): + #cr.execute(""" + #create or replace view report_hr_timesheet_invoice_journal as ( + # select + # min(l.id) as id, + # substring(l.create_date for 7)||'-01' as name, + # sum(greatest(-l.amount,0)) as cost, + # sum(greatest(l.amount,0)) as revenue, + # sum(l.unit_amount*u.factor) as quantity, + # journal_id, + # account_id + # from account_analytic_line l + # left join product_uom u on (u.id=l.product_uom_id) + # group by + # substring(l.create_date for 7), + # journal_id, + # account_id + #)""") + cr.execute(""" + create or replace view report_hr_timesheet_invoice_journal as ( + select + min(l.id) as id, + substring(l.create_date for 7)||'-01' as name, + sum( + CASE WHEN -l.amount>0 THEN 0 ELSE -l.amount + END + ) as cost, + sum( + CASE WHEN l.amount>0 THEN l.amount ELSE 0 + END + ) as revenue, + sum(l.unit_amount*u.factor) as quantity, + journal_id, + account_id + from account_analytic_line l + left join product_uom u on (u.id=l.product_uom_id) + group by + substring(l.create_date for 7), + journal_id, + account_id + )""") +timesheet_invoice() diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py new file mode 100644 index 00000000000..ba6e4b231b8 --- /dev/null +++ b/addons/account/account_bank_statement.py @@ -0,0 +1,192 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved. +# +# $Id: account.py 1005 2005-07-25 08:41:42Z nicoe $ +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## +import time +import netsvc +from osv import fields, osv + +from tools.misc import currency + +import mx.DateTime +from mx.DateTime import RelativeDateTime, now, DateTime, localtime + +# +# use a sequence for names ? +# +class account_bank_statement(osv.osv): + def _default_journal_id(self, cr, uid, context={}): + if context.get('journal_id', False): + return context['journal_id'] + return False + + def _default_balance_start(self, cr, uid, context={}): + cr.execute('select id from account_bank_statement where journal_id=%d order by date desc limit 1', (1,)) + res = cr.fetchone() + if res: + return self.browse(cr, uid, [res[0]], context)[0].balance_end + return 0.0 + + def _end_balance(self, cr, uid, ids, prop, unknow_none, unknow_dict): + res = {} + statements = self.browse(cr, uid, ids) + for statement in statements: + res[statement.id] = statement.balance_start + for line in statement.line_ids: + res[statement.id] += line.amount + for r in res: + res[r] = round(res[r], 2) + return res + + def _get_period(self, cr, uid, context={}): + periods = self.pool.get('account.period').find(cr, uid) + if periods: + return periods[0] + else: + return False + + _order = "date desc" + _name = "account.bank.statement" + _description = "Bank Statement" + _columns = { + 'name': fields.char('Name', size=64, required=True), + 'date': fields.date('Date', required=True, states={'confirm':[('readonly',True)]}), + 'journal_id': fields.many2one('account.journal', 'Journal', required=True, states={'confirm':[('readonly',True)]}, domain=[('type','=','cash')], relate=True), + 'period_id': fields.many2one('account.period', 'Period', required=True, states={'confirm':[('readonly',True)]}), + 'balance_start': fields.float('Starting Balance', digits=(16,2), states={'confirm':[('readonly',True)]}), + 'balance_end_real': fields.float('Ending Balance', digits=(16,2), states={'confirm':[('readonly',True)]}), + 'balance_end': fields.function(_end_balance, method=True, string='Balance'), + 'line_ids': fields.one2many('account.bank.statement.line', 'statement_id', 'Statement lines', states={'confirm':[('readonly',True)]}), + 'move_line_ids': fields.one2many('account.move.line', 'statement_id', 'Entry lines', states={'confirm':[('readonly',True)]}), + 'state': fields.selection([('draft','Draft'),('confirm','Confirm')], 'State', required=True, states={'confirm':[('readonly',True)]}, readonly="1"), + } + + _defaults = { + 'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement'), + 'date': lambda *a: time.strftime('%Y-%m-%d'), + 'state': lambda *a: 'draft', + 'balance_start': _default_balance_start, + 'journal_id': _default_journal_id, + 'period_id': _get_period, + } + def button_confirm(self, cr, uid, ids, context={}): + done = [] + for st in self.browse(cr, uid, ids, context): + if not st.state=='draft': + continue + if not (abs(st.balance_end - st.balance_end_real) < 0.0001): + raise osv.except_osv('Error !', 'The statement balance is incorrect !\nCheck that the ending balance equals the computed one.') + if (not st.journal_id.default_credit_account_id) or (not st.journal_id.default_debit_account_id): + raise osv.except_osv('Configration Error !', 'Please verify that an account is defined in the journal.') + for move in st.line_ids: + if not move.amount: + continue + self.pool.get('account.move.line').create(cr, uid, { + 'name': move.name, + 'date': move.date, + 'partner_id': ((move.partner_id) and move.partner_id.id) or False, + 'account_id': (move.account_id) and move.account_id.id, + 'credit': ((move.amount>0) and move.amount) or 0.0, + 'debit': ((move.amount<0) and -move.amount) or 0.0, + 'statement_id': st.id, + 'journal_id': st.journal_id.id, + 'period_id': st.period_id.id, + }, context=context) + if not st.journal_id.centralisation: + c = context.copy() + c['journal_id'] = st.journal_id.id + c['period_id'] = st.period_id.id + fields = ['move_id','name','date','partner_id','account_id','credit','debit'] + default = self.pool.get('account.move.line').default_get(cr, uid, fields, context=c) + default.update({ + 'statement_id': st.id, + 'journal_id': st.journal_id.id, + 'period_id': st.period_id.id, + }) + self.pool.get('account.move.line').create(cr, uid, default, context=context) + done.append(st.id) + self.write(cr, uid, done, {'state':'confirm'}, context=context) + return True + def button_cancel(self, cr, uid, ids, context={}): + done = [] + for st in self.browse(cr, uid, ids, context): + if st.state=='draft': + continue + ids = [x.move_id.id for x in st.move_line_ids] + self.pool.get('account.move').unlink(cr, uid, ids, context) + done.append(st.id) + self.write(cr, uid, done, {'state':'draft'}, context=context) + return True + def onchange_journal_id(self, cr, uid, id, journal_id, context={}): + if not journal_id: + return {} + cr.execute('select balance_end_real from account_bank_statement where journal_id=%d order by date desc limit 1', (journal_id,)) + res = cr.fetchone() + if res: + return {'value': {'balance_start': res[0] or 0.0}} + return {} +account_bank_statement() + +class account_bank_statement_line(osv.osv): + def onchange_partner_id(self, cr, uid, id, partner_id, type, context={}): + if not partner_id: + return {} + part = self.pool.get('res.partner').browse(cr, uid, partner_id, context) + if type=='supplier': + account_id = part.property_account_payable[0] + else: + account_id = part.property_account_receivable[0] + cr.execute('select sum(debit-credit) from account_move_line where (reconcile_id is null) and partner_id=%d and account_id=%d', (partner_id, account_id)) + balance = cr.fetchone()[0] or 0.0 + val = {'amount': balance, 'account_id':account_id} + return {'value':val} + _order = "date,name desc" + _name = "account.bank.statement.line" + _description = "Bank Statement Line" + _columns = { + 'name': fields.char('Name', size=64, required=True), + 'date': fields.date('Date'), + 'amount': fields.float('Amount'), + 'type': fields.selection([('supplier','Supplier'),('customer','Customer'),('general','General')], 'Type', required=True), + 'partner_id': fields.many2one('res.partner', 'Partner'), + 'account_id': fields.many2one('account.account','Account', required=True), + 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True), + 'invoice_id': fields.many2one('account.invoice', 'Invoice', states={'confirm':[('readonly',True)]}), + + + } + _defaults = { + 'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement.line'), + 'date': lambda *a: time.strftime('%Y-%m-%d'), + 'type': lambda *a: 'general', + } +account_bank_statement_line() + + + + diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml index abd0c2b16ce..8e94e7de722 100644 --- a/addons/account/account_invoice_view.xml +++ b/addons/account/account_invoice_view.xml @@ -105,11 +105,13 @@ - + + + @@ -138,6 +140,7 @@ + diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py new file mode 100644 index 00000000000..3e053d7b1fe --- /dev/null +++ b/addons/account/account_move_line.py @@ -0,0 +1,466 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved. +# +# $Id: account.py 1005 2005-07-25 08:41:42Z nicoe $ +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## +import time +import netsvc +from osv import fields, osv + + +class account_move_line(osv.osv): + _name = "account.move.line" + _description = "Entry lines" + + def default_get(self, cr, uid, fields, context={}): + data = self._default_get(cr, uid, fields, context) + for f in data.keys(): + if f not in fields: + del data[f] + return data + + def _default_get(self, cr, uid, fields, context={}): + # Compute simple values + data = super(account_move_line, self).default_get(cr, uid, fields, context) + + # Compute the current move + move_id = False + partner_id = False + statement_acc_id = False + if context.get('journal_id',False) and context.get('period_id',False): + cr.execute('select move_id \ + from \ + account_move_line \ + where \ + journal_id=%d and period_id=%d and create_uid=%d and state=%s \ + order by id desc limit 1', (context['journal_id'], context['period_id'], uid, 'draft')) + res = cr.fetchone() + move_id = (res and res[0]) or False + cr.execute('select date \ + from \ + account_move_line \ + where \ + journal_id=%d and period_id=%d and create_uid=%d order by id desc', (context['journal_id'], context['period_id'], uid)) + res = cr.fetchone() + data['date'] = res and res[0] or time.strftime('%Y-%m-%d') + cr.execute('select statement_id, account_id \ + from \ + account_move_line \ + where \ + journal_id=%d and period_id=%d and statement_id is not null and create_uid=%d order by id desc', (context['journal_id'], context['period_id'], uid)) + res = cr.fetchone() + statement_id = res and res[0] or False + statement_acc_id = res and res[1] + + if not move_id: + return data + + data['move_id'] = move_id + + total = 0 + taxes = {} + move = self.pool.get('account.move').browse(cr, uid, move_id, context) + for l in move.line_id: + partner_id = partner_id or l.partner_id.id + total += (l.debit - l.credit) + for tax in l.account_id.tax_ids: + acc = (l.debit >0) and tax.account_paid_id.id or tax.account_collected_id.id + taxes.setdefault((acc,tax.tax_code_id.id), False) + taxes[(l.account_id.id,l.tax_code_id.id)] = True + data.setdefault('name', l.name) + + data['partner_id'] = partner_id + + print taxes + for t in taxes: + if not taxes[t] and t[0]: + s=0 + for l in move.line_id: + for tax in l.account_id.tax_ids: + taxes = self.pool.get('account.tax').compute(cr, uid, [tax.id], l.debit or l.credit, 1, False) + key = (l.debit and 'account_paid_id') or 'account_collected_id' + for t2 in taxes: + if (t2[key] == t[0]) and (tax.tax_code_id.id==t[1]): + if l.debit: + s += t2['amount'] + else: + s -= t2['amount'] + data['debit'] = s>0 and s or 0.0 + data['credit'] = s<0 and -s or 0.0 + + data['tax_code_id'] = t[1] + + data['account_id'] = t[0] + + # + # Compute line for tax T + # + return data + + # + # Compute latest line + # + data['credit'] = total>0 and total + data['debit'] = total<0 and -total + if total>=0: + data['account_id'] = move.journal_id.default_credit_account_id.id or False + else: + data['account_id'] = move.journal_id.default_debit_account_id.id or False + if data['account_id']: + account = self.pool.get('account.account').browse(cr, uid, data['account_id']) + data['tax_code_id'] = self._default_get_tax(cr, uid, account ) + return data + + def _default_get_tax(self, cr, uid, account, debit=0, credit=0, context={}): + if account.tax_ids: + return account.tax_ids[0].base_code_id.id + return False + + def _on_create_write(self, cr, uid, id, context={}): + ml = self.browse(cr, uid, id, context) + return map(lambda x: x.id, ml.move_id.line_id) + + def _balance(self, cr, uid, ids, prop, unknow_none, unknow_dict): + res={} + # TODO group the foreach in sql + for id in ids: + cr.execute('SELECT date,account_id FROM account_move_line WHERE id=%d', (id,)) + dt, acc = cr.fetchone() + cr.execute('SELECT SUM(debit-credit) FROM account_move_line WHERE account_id=%d AND (date<%s OR (date=%s AND id<=%d)) and active', (acc,dt,dt,id)) + res[id] = cr.fetchone()[0] + return res + + _columns = { + 'name': fields.char('Name', size=64, required=True), + 'quantity': fields.float('Quantity', digits=(16,2), help="The optionnal quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very usefull for some reports."), + 'debit': fields.float('Debit', digits=(16,2), states={'reconciled':[('readonly',True)]}), + 'credit': fields.float('Credit', digits=(16,2), states={'reconciled':[('readonly',True)]}), + 'account_id': fields.many2one('account.account', 'Account', required=True, ondelete="cascade", states={'reconciled':[('readonly',True)]}, domain=[('type','<>','view')]), + + 'move_id': fields.many2one('account.move', 'Entry', required=True, ondelete="cascade", states={'reconciled':[('readonly',True)]}, help="The entry of this entry line.", select=True), + + 'ref': fields.char('Ref.', size=32), + 'statement_id': fields.many2one('account.bank.statement', 'Statement', help="The bank statement used for bank reconciliation", select=True), + 'reconcile_id': fields.many2one('account.move.reconcile', 'Reconcile', readonly=True, ondelete='set null', select=True), + 'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optionnal other currency if it is a multi-currency entry."), + 'currency_id': fields.many2one('res.currency', 'Currency', help="The optionnal other currency if it is a multi-currency entry."), + + 'period_id': fields.many2one('account.period', 'Period', required=True), + 'journal_id': fields.many2one('account.journal', 'Journal', required=True, relate=True), + 'blocked': fields.boolean('Litigation', help="You can check this box to mark the entry line as a litigation with the associated partner"), + + 'partner_id': fields.many2one('res.partner', 'Partner Ref.', states={'reconciled':[('readonly',True)]}), + 'date_maturity': fields.date('Maturity date', states={'reconciled':[('readonly',True)]}, help="This field is used for payable and receivable entries. You can put the limit date for the payment of this entry line."), + 'date': fields.date('Effective date', required=True), + 'date_created': fields.date('Creation date'), + 'analytic_lines': fields.one2many('account.analytic.line', 'move_id', 'Analytic lines'), + 'centralisation': fields.selection([('normal','Normal'),('credit','Credit Centralisation'),('debit','Debit Centralisation')], 'Centralisation', size=6), + 'balance': fields.function(_balance, method=True, string='Balance'), + 'active': fields.boolean('Active'), + 'state': fields.selection([('draft','Draft'), ('valid','Valid'), ('reconciled','Reconciled')], 'State', readonly=True), + 'tax_code_id': fields.many2one('account.tax.code', 'Tax Account'), + 'tax_amount': fields.float('Tax/Base Amount', digits=(16,2), select=True), + } + _defaults = { + 'blocked': lambda *a: False, + 'active': lambda *a: True, + 'centralisation': lambda *a: 'normal', + 'date_created': lambda *a: time.strftime('%Y-%m-%d'), + 'state': lambda *a: 'draft', + 'journal_id': lambda self, cr, uid, c: c.get('journal_id', False), + 'period_id': lambda self, cr, uid, c: c.get('period_id', False), + } + _order = "date desc,id desc" + _sql_constraints = [ + ('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'), + ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'), + ] + def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, journal=False): + if (not partner_id) or account_id: + return {} + part = self.pool.get('res.partner').browse(cr, uid, partner_id) + id1 = part.property_account_payable[0] + id2 = part.property_account_receivable[0] + cr.execute('select sum(debit-credit) from account_move_line where (reconcile_id is null) and partner_id=%d and account_id=%d', (partner_id, id2)) + balance = cr.fetchone()[0] or 0.0 + val = {} + if (not debit) and (not credit): + if abs(balance)>0.01: + val['credit'] = ((balance>0) and balance) or 0 + val['debit'] = ((balance<0) and -balance) or 0 + val['account_id'] = id2 + else: + cr.execute('select sum(debit-credit) from account_move_line where (reconcile_id is null) and partner_id=%d and account_id=%d', (partner_id, id1)) + balance = cr.fetchone()[0] or 0.0 + val['credit'] = ((balance>0) and balance) or 0 + val['debit'] = ((balance<0) and -balance) or 0 + val['account_id'] = id1 + else: + val['account_id'] = (debit>0) and id2 or id1 + if journal: + jt = self.pool.get('account.journal').browse(cr, uid, journal).type + if jt=='sale': + val['account_id'] = id2 + elif jt=='purchase': + val['account_id'] = id1 + return {'value':val} + + # + # type: the type if reconciliation (no logic behind this field, for infà) + # + # writeoff; entry generated for the difference between the lines + # + + def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context={}): + id_set = ','.join(map(str, ids)) + lines = self.read(cr, uid, ids, context=context) + unrec_lines = filter(lambda x: not x['reconcile_id'], lines) + credit = debit = 0 + account_id = False + partner_id = False + for line in unrec_lines: + credit += line['credit'] + debit += line['debit'] + account_id = line['account_id'][0] + partner_id = (line['partner_id'] and line['partner_id'][0]) or False + writeoff = debit - credit + date = time.strftime('%Y-%m-%d') + + cr.execute('SELECT account_id,reconcile_id FROM account_move_line WHERE id IN ('+id_set+') GROUP BY account_id,reconcile_id') + r = cr.fetchall() +#TODO: move this check to a constraint in the account_move_reconcile object + if len(r) != 1: + raise Exception('Entries are not of the same account or already reconciled ! ') + if r[0][1] != None: + raise Exception('Some entries are already reconciled !') + if writeoff != 0: + if not writeoff_acc_id: + raise osv.except_osv('Warning', 'You have to provide an account for the write off entry !') + if writeoff > 0: + debit = writeoff + credit = 0.0 + self_credit = writeoff + self_debit = 0.0 + else: + debit = 0.0 + credit = -writeoff + self_credit = 0.0 + self_debit = -writeoff + + writeoff_lines = [ + (0, 0, {'name':'Write-Off', 'debit':self_debit, 'credit':self_credit, 'account_id':account_id, 'date':date, 'partner_id':partner_id}), + (0, 0, {'name':'Write-Off', 'debit':debit, 'credit':credit, 'account_id':writeoff_acc_id, 'date':date, 'partner_id':partner_id}) + ] + + name = 'Write-Off' + if writeoff_journal_id: + journal = self.pool.get('account.journal').browse(cr, uid, writeoff_journal_id) + if journal.sequence_id: + name = self.pool.get('ir.sequence').get_id(cr, uid, journal.sequence_id.id) + + writeoff_move_id = self.pool.get('account.move').create(cr, uid, { + 'name': name, + 'period_id': writeoff_period_id, + 'journal_id': writeoff_journal_id, + + 'state': 'draft', + 'line_id': writeoff_lines + }) + + writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)]) + ids += writeoff_line_ids + + self.write(cr, uid, ids, {'state': 'reconciled'}, update_check=False) + r_id = self.pool.get('account.move.reconcile').create(cr, uid, { + 'name': date, + 'type': type, + 'line_id': map(lambda x: (4,x,False), ids) + }) + # the id of the move.reconcile is written in the move.line (self) by the create method above + # because of the way the line_id are defined: (4, x, False) + wf_service = netsvc.LocalService("workflow") + for id in ids: + wf_service.trg_trigger(uid, 'account.move.line', id, cr) + return r_id + + def view_header_get(self, cr, user, view_id, view_type, context): + if (not context.get('journal_id', False)) or (not context.get('period_id', False)): + return False + cr.execute('select code from account_journal where id=%d', (context['journal_id'],)) + j = cr.fetchone()[0] or '' + cr.execute('select code from account_period where id=%d', (context['period_id'],)) + p = cr.fetchone()[0] or '' + if j or p: + return j+':'+p + return 'Journal' + + def fields_view_get(self, cr, uid, view_id=None, view_type='form', context={}, toolbar=False): + result = super(osv.osv, self).fields_view_get(cr, uid, view_id,view_type,context) + if view_type=='tree' and 'journal_id' in context: + title = self.view_header_get(cr, uid, view_id, view_type, context) + journal = self.pool.get('account.journal').browse(cr, uid, context['journal_id']) + + # if the journal view has a state field, color lines depending on + # its value + state = '' + for field in journal.view_id.columns_id: + if field.field=='state': + state = ' colors="red:state==\'draft\'"' + + #xml = '''\n\n\t''' % (title, state) + xml = '''\n\n\t''' % (title, state) + fields = [] + + widths = { + 'ref': 50, + 'statement_id': 50, + 'state': 60, + 'tax_code_id': 50, + 'move_id': 40, + } + for field in journal.view_id.columns_id: + fields.append(field.field) + attrs = [] + if field.readonly: + attrs.append('readonly="1"') + if field.required: + attrs.append('required="1"') + else: + attrs.append('required="0"') + if field.field == 'partner_id': + attrs.append('on_change="onchange_partner_id(move_id,partner_id,account_id,debit,credit,((\'journal_id\' in context) and context[\'journal_id\']) or {})"') + if field.field in widths: + attrs.append('width="'+str(widths[field.field])+'"') + xml += '''\n''' % (field.field,' '.join(attrs)) + + xml += '''''' + result['arch'] = xml + result['fields'] = self.fields_get(cr, uid, fields, context) + return result + + def unlink(self, cr, uid, ids, context={}, check=True): + self._update_check(cr, uid, ids, context) + for line in self.browse(cr, uid, ids, context): + context['journal_id']=line.journal_id.id + context['period_id']=line.period_id.id + result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context) + if check: + self.pool.get('account.move').validate(cr, uid, [line.move_id.id], context=context) + return result + + # + # TO VERIFY: check if try to write journal of only one line ??? + # + def write(self, cr, uid, ids, vals, context={}, check=True, update_check=True): + if update_check: + self._update_check(cr, uid, ids, context) + result = super(osv.osv, self).write(cr, uid, ids, vals, context) + if check: + done = [] + for line in self.browse(cr, uid, ids): + if line.move_id.id not in done: + done.append(line.move_id.id) + self.pool.get('account.move').validate(cr, uid, [line.move_id.id], context) + return result + + def _update_journal_check(self, cr, uid, journal_id, period_id, context={}): + cr.execute('select state from account_journal_period where journal_id=%d and period_id=%d', (journal_id, period_id)) + result = cr.fetchall() + for (state,) in result: + if state=='done': + raise osv.except_osv('Error !', 'You can not add/modify entries in a closed journal.') + if not result: + journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context) + period = self.pool.get('account.period').browse(cr, uid, period_id, context) + self.pool.get('account.journal.period').create(cr, uid, { + 'name': (journal.code or journal.name)+':'+(period.name or ''), + 'journal_id': journal.id, + 'period_id': period.id + }) + return True + + def _update_check(self, cr, uid, ids, context={}): + done = {} + for line in self.browse(cr, uid, ids, context): + if line.move_id.state<>'draft': + raise osv.except_osv('Error !', 'You can not modify or delete a confirmed entry !') + if line.reconcile_id: + raise osv.except_osv('Error !', 'You can not modify or delete a reconciled entry !') + t = (line.journal_id.id, line.period_id.id) + if t not in done: + self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context) + done[t] = True + return True + + def create(self, cr, uid, vals, context={}, check=True): + if 'journal_id' in vals and 'journal_id' not in context: + context['journal_id'] = vals['journal_id'] + if 'period_id' in vals and 'period_id' not in context: + context['period_id'] = vals['period_id'] + if 'journal_id' not in context and 'move_id' in vals: + m = self.pool.get('account.move').browse(cr, uid, vals['move_id']) + context['journal_id'] = m.journal_id.id + context['period_id'] = m.period_id.id + self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context) + move_id = vals.get('move_id', False) + journal = self.pool.get('account.journal').browse(cr, uid, context['journal_id']) + if not move_id: + if journal.centralisation: + # use the first move ever created for this journal and period + cr.execute('select id from account_move where journal_id=%d and period_id=%d order by id limit 1', (context['journal_id'],context['period_id'])) + res = cr.fetchone() + if res: + vals['move_id'] = res[0] + + if not vals.get('move_id', False): + if journal.sequence_id: + name = self.pool.get('ir.sequence').get_id(cr, uid, journal.sequence_id.id) + v = { + 'name': name, + 'period_id': context['period_id'], + 'journal_id': context['journal_id'] + } + move_id = self.pool.get('account.move').create(cr, uid, v, context) + vals['move_id'] = move_id + else: + raise osv.except_osv('No piece number !', 'Can not create an automatic sequence for this piece !\n\nPut a sequence in the journal definition for automatic numbering or create a sequence manually for this piece.') + + if ('account_id' in vals) and journal.type_control_ids: + type = self.pool.get('account.account').browse(cr, uid, vals['account_id']).type + ok = False + for t in journal.type_control_ids: + if type==t.code: + ok = True + break + if not ok: + raise osv.except_osv('Bad account !', 'You can not use this general account in this journal !') + + result = super(osv.osv, self).create(cr, uid, vals, context) + if check: + self.pool.get('account.move').validate(cr, uid, [vals['move_id']], context) + return result +account_move_line() diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 7a689cef425..c21e1df901f 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -398,7 +398,7 @@ - + @@ -407,6 +407,7 @@ + diff --git a/addons/account/invoice.py b/addons/account/invoice.py index aeff689ca77..7e90721ca99 100644 --- a/addons/account/invoice.py +++ b/addons/account/invoice.py @@ -107,6 +107,8 @@ class account_invoice(osv.osv): 'date_invoice': fields.date('Date Invoiced', required=True, states={'open':[('readonly',True)],'close':[('readonly',True)]}), 'date_due': fields.date('Due Date', states={'open':[('readonly',True)],'close':[('readonly',True)]}), + 'date_discount': fields.date('Discount Date', states={'open':[('readonly',True)],'close':[('readonly',True)]}, + help='The date of the first cash discount'), 'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}, relate=True), 'address_contact_id': fields.many2one('res.partner.address', 'Contact Address', readonly=True, states={'draft':[('readonly',False)]}), @@ -175,16 +177,32 @@ class account_invoice(osv.osv): def onchange_currency_id(self, cr, uid, ids, curr_id): return {} - def onchange_payment_term(self, cr, uid, ids, payment_term): - if not payment_term: + def onchange_payment_term(self, cr, uid, ids, payment_term_id): + if not payment_term_id: return {} - invoice= self.pool.get('account.invoice').browse(cr, uid, ids)[0] - pterm_list= self.pool.get('account.payment.term').compute(cr, uid, payment_term, value=1, date_ref=invoice.date_invoice) - if not pterm_list: - return {} - pterm_list = [line[0] for line in pterm_list] - pterm_list.sort() - return {'value':{'date_due': pterm_list[-1]}} + res={} + pt_obj= self.pool.get('account.payment.term') + + if ids : + invoice= self.pool.get('account.invoice').browse(cr, uid, ids)[0] + date_invoice= invoice.date_invoice + else: + date_invoice= time.strftime('%Y-%m-%d') + + pterm_list= pt_obj.compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice) + + if pterm_list: + pterm_list = [line[0] for line in pterm_list] + pterm_list.sort() + res= {'value':{'date_due': pterm_list[-1]}} + + + disc_list= pt_obj.get_discounts(cr,uid,payment_term_id,date_invoice) + if disc_list : + res = res or {'value':{}} + res['value'].update({'date_discount': disc_list[0][0] }) + + return res # go from canceled state to draft state diff --git a/addons/account/project/project.py b/addons/account/project/project.py index 929e40145ee..5af1aa708c5 100644 --- a/addons/account/project/project.py +++ b/addons/account/project/project.py @@ -174,99 +174,5 @@ class account_analytic_journal(osv.osv): 'type': lambda *a: 'general', } account_analytic_journal() - -class account_analytic_line(osv.osv): - _name = 'account.analytic.line' - _columns = { - 'name' : fields.char('Description', size=128, required=True), - 'date' : fields.date('Date', required=True), - 'amount' : fields.float('Amount', required=True), - 'unit_amount' : fields.float('Quantity'), - 'product_uom_id' : fields.many2one('product.uom', 'UoM'), - 'product_id' : fields.many2one('product.product', 'Product'), - 'account_id' : fields.many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='cascade', select=True), - 'general_account_id' : fields.many2one('account.account', 'General account', required=True, ondelete='cascade'), - 'move_id' : fields.many2one('account.move.line', 'General entry', ondelete='cascade', select=True), - 'journal_id' : fields.many2one('account.analytic.journal', 'Analytic journal', required=True, ondelete='cascade', select=True), - 'code' : fields.char('Code', size=8), - 'user_id' : fields.many2one('res.users', 'User',), - } - - _defaults = { - 'date': lambda *a: time.strftime('%Y-%m-%d'), - } - _order = 'date' - - def on_change_unit_amount(self, cr, uid, id, prod_id, unit_amount, unit=False, context={}): - if unit_amount and prod_id: - rate = 1 - if unit: - uom_id = self.pool.get('product.uom') - hunit = uom_id.browse(cr, uid, unit) - rate = hunit.factor - uom_id = self.pool.get('product.product') - prod = uom_id.browse(cr, uid, prod_id) - a = prod.product_tmpl_id.property_account_expense - if not a: - a = prod.categ_id.property_account_expense_categ - return {'value' : {'amount' : -round(unit_amount * prod.standard_price * rate,2), 'general_account_id':a[0]}} - return {} -account_analytic_line() - -class timesheet_invoice(osv.osv): - _name = "report.hr.timesheet.invoice.journal" - _description = "Analytic account costs and revenues" - _auto = False - _columns = { - 'name': fields.date('Month', readonly=True), - 'account_id':fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, relate=True, select=True), - 'journal_id': fields.many2one('account.analytic.journal', 'Journal', readonly=True), - 'quantity': fields.float('Quantities', readonly=True), - 'cost': fields.float('Credit', readonly=True), - 'revenue': fields.float('Debit', readonly=True) - } - _order = 'name desc, account_id' - def init(self, cr): - #cr.execute(""" - #create or replace view report_hr_timesheet_invoice_journal as ( - # select - # min(l.id) as id, - # substring(l.create_date for 7)||'-01' as name, - # sum(greatest(-l.amount,0)) as cost, - # sum(greatest(l.amount,0)) as revenue, - # sum(l.unit_amount*u.factor) as quantity, - # journal_id, - # account_id - # from account_analytic_line l - # left join product_uom u on (u.id=l.product_uom_id) - # group by - # substring(l.create_date for 7), - # journal_id, - # account_id - #)""") - cr.execute(""" - create or replace view report_hr_timesheet_invoice_journal as ( - select - min(l.id) as id, - substring(l.create_date for 7)||'-01' as name, - sum( - CASE WHEN -l.amount>0 THEN 0 ELSE -l.amount - END - ) as cost, - sum( - CASE WHEN l.amount>0 THEN l.amount ELSE 0 - END - ) as revenue, - sum(l.unit_amount*u.factor) as quantity, - journal_id, - account_id - from account_analytic_line l - left join product_uom u on (u.id=l.product_uom_id) - group by - substring(l.create_date for 7), - journal_id, - account_id - )""") -timesheet_invoice() diff --git a/addons/l10n_ch/dta/dta_wizard.py b/addons/l10n_ch/dta/dta_wizard.py index a0a17fba3cb..6cab088c4cd 100644 --- a/addons/l10n_ch/dta/dta_wizard.py +++ b/addons/l10n_ch/dta/dta_wizard.py @@ -43,15 +43,6 @@ def _bank_get(self, cr, uid, context={}): res = [(r['active'], r['name']) for r in res] return res -def _journal_get(self, cr, uid, context={}): - pool= pooler.get_pool(cr.dbname) - obj= pool.get('account.journal') - ids= obj.search(cr, uid, [('type','=','cash')]) - res= obj.read(cr, uid, ids, ['active', 'name'], context) - res= [(r['active'], r['name']) for r in res] - return res - - check_form = """
@@ -61,6 +52,7 @@ check_form = """ """ + check_fields = { 'dta_line_ids' : { 'string':'DTA lines', @@ -76,9 +68,11 @@ check_fields = { 'journal' : { 'string':'Journal', - 'type':'selection', - 'selection':_journal_get, + 'type':'many2one', + 'relation':'account.journal', + 'domain':"[('type','=','cash')]", 'required': True, + 'help':'The journal used for the bank statement', }, } @@ -146,26 +140,25 @@ def _get_dta_lines(self,cr,uid,data,context): if i.dta_state != '2bpaid' or i.state in ['draft','cancel','paid']: continue - discount_ids= i.payment_term and i.payment_term.cash_discount_ids and i.payment_term.cash_discount_ids - discount= "" cash_disc_date="" - if discount_ids: - for d in discount_ids: # TODO a mettre en fct dans l'objet payment term - cash_disc_date= mx.DateTime.strptime(i.date_invoice,'%Y-%m-%d') +\ - RelativeDateTime(days=d.delay+1) - if cash_disc_date >= mx.DateTime.now(): - discount= d + discount="" + if i.payment_term : + disc_list= pool.get('account.payment.term').get_discounts(cr,uid,i.payment_term.id, i.date_invoice) + + for (cash_disc_date,discount) in disc_list: + if cash_disc_date >= time.strftime('%Y-%m-%d'): break + lines.append(dta_line_obj.create(cr,uid,{ 'name': i.id, 'partner_id': i.partner_id.id, 'due_date': i.date_due, 'invoice_date': i.date_invoice, - 'cashdisc_date': discount and cash_disc_date.strftime('%Y-%m-%d') or None, - 'amount_to_pay': discount and i.amount_total*(1-discount.discount) or i.amount_total, + 'cashdisc_date': cash_disc_date and cash_disc_date or None, + 'amount_to_pay': discount and i.amount_total*(1-discount) or i.amount_total, 'amount_invoice': i.amount_total, - 'amount_cashdisc': discount and i.amount_total*(1-discount.discount), + 'amount_cashdisc': discount and i.amount_total*(1-discount), 'dta_id': id_dta, })) @@ -311,14 +304,13 @@ def _create_dta(self,cr,uid,data,context): date_value = dtal.cashdisc_date or dtal.due_date or "" date_value = date_value and mx.DateTime.strptime( date_value,'%Y-%m-%d') or mx.DateTime.now() - #date_value = date_value.strftime("%y%m%d") try: #header hdr= header('000000','',creation_date,company_iban,'idfi',seq,'836','0') # TODO id_file - # segment 01: - dta_line = ''.join([segment_01(hdr,company_dta, + + dta_line = ''.join([segment_01(hdr,company_dta,# segment 01: number,company_iban,date_value.strftime("%y%m%d") ,currency,str(dtal.amount_to_pay)), # adresse donneur d'ordre segment_02(company.name,co_addr.street,co_addr.zip,co_addr.city,country,cours=''),# donnees de la banque @@ -346,6 +338,7 @@ def _create_dta(self,cr,uid,data,context): 'partner_id':i.partner_id.id, 'account_id':i.account_id.id, 'statement_id': bk_st_id, + 'invoice_id': i.id, }) dta = dta + dta_line