[IMP] Account : changed terms as suggested in account module
[odoo/odoo.git] / addons / account / partner.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from operator import itemgetter
23 import time
24
25 from openerp.osv import fields, osv
26
27 class account_fiscal_position(osv.osv):
28     _name = 'account.fiscal.position'
29     _description = 'Fiscal Position'
30     _columns = {
31         'name': fields.char('Fiscal Position', size=64, required=True),
32         'active': fields.boolean('Active', help="By unchecking the active field, you may hide a fiscal position without deleting it."),
33         'company_id': fields.many2one('res.company', 'Company'),
34         'account_ids': fields.one2many('account.fiscal.position.account', 'position_id', 'Account Mapping'),
35         'tax_ids': fields.one2many('account.fiscal.position.tax', 'position_id', 'Tax Mapping'),
36         'note': fields.text('Notes', translate=True),
37     }
38
39     _defaults = {
40         'active': True,
41     }
42
43     def map_tax(self, cr, uid, fposition_id, taxes, context=None):
44         if not taxes:
45             return []
46         if not fposition_id:
47             return map(lambda x: x.id, taxes)
48         result = set()
49         for t in taxes:
50             ok = False
51             for tax in fposition_id.tax_ids:
52                 if tax.tax_src_id.id == t.id:
53                     if tax.tax_dest_id:
54                         result.add(tax.tax_dest_id.id)
55                     ok=True
56             if not ok:
57                 result.add(t.id)
58         return list(result)
59
60     def map_account(self, cr, uid, fposition_id, account_id, context=None):
61         if not fposition_id:
62             return account_id
63         for pos in fposition_id.account_ids:
64             if pos.account_src_id.id == account_id:
65                 account_id = pos.account_dest_id.id
66                 break
67         return account_id
68
69 account_fiscal_position()
70
71 class account_fiscal_position_tax(osv.osv):
72     _name = 'account.fiscal.position.tax'
73     _description = 'Taxes Fiscal Position'
74     _rec_name = 'position_id'
75     _columns = {
76         'position_id': fields.many2one('account.fiscal.position', 'Fiscal Position', required=True, ondelete='cascade'),
77         'tax_src_id': fields.many2one('account.tax', 'Tax Source', required=True),
78         'tax_dest_id': fields.many2one('account.tax', 'Replacement Tax')
79     }
80
81     _sql_constraints = [
82         ('tax_src_dest_uniq',
83          'unique (position_id,tax_src_id,tax_dest_id)',
84          'A tax fiscal position could be defined only once time on same taxes.')
85     ]
86
87 account_fiscal_position_tax()
88
89 class account_fiscal_position_account(osv.osv):
90     _name = 'account.fiscal.position.account'
91     _description = 'Accounts Fiscal Position'
92     _rec_name = 'position_id'
93     _columns = {
94         'position_id': fields.many2one('account.fiscal.position', 'Fiscal Position', required=True, ondelete='cascade'),
95         'account_src_id': fields.many2one('account.account', 'Account Source', domain=[('type','<>','view')], required=True),
96         'account_dest_id': fields.many2one('account.account', 'Account Destination', domain=[('type','<>','view')], required=True)
97     }
98
99     _sql_constraints = [
100         ('account_src_dest_uniq',
101          'unique (position_id,account_src_id,account_dest_id)',
102          'An account fiscal position could be defined only once time on same accounts.')
103     ]
104
105 account_fiscal_position_account()
106
107 class res_partner(osv.osv):
108     _name = 'res.partner'
109     _inherit = 'res.partner'
110     _description = 'Partner'
111
112     def _credit_debit_get(self, cr, uid, ids, field_names, arg, context=None):
113         query = self.pool.get('account.move.line')._query_get(cr, uid, context=context)
114         cr.execute("""SELECT l.partner_id, a.type, SUM(l.debit-l.credit)
115                       FROM account_move_line l
116                       LEFT JOIN account_account a ON (l.account_id=a.id)
117                       WHERE a.type IN ('receivable','payable')
118                       AND l.partner_id IN %s
119                       AND l.reconcile_id IS NULL
120                       AND """ + query + """
121                       GROUP BY l.partner_id, a.type
122                       """,
123                    (tuple(ids),))
124         maps = {'receivable':'credit', 'payable':'debit' }
125         res = {}
126         for id in ids:
127             res[id] = {}.fromkeys(field_names, 0)
128         for pid,type,val in cr.fetchall():
129             if val is None: val=0
130             res[pid][maps[type]] = (type=='receivable') and val or -val
131         return res
132
133     def _asset_difference_search(self, cr, uid, obj, name, type, args, context=None):
134         if not args:
135             return []
136         having_values = tuple(map(itemgetter(2), args))
137         where = ' AND '.join(
138             map(lambda x: '(SUM(bal2) %(operator)s %%s)' % {
139                                 'operator':x[1]},args))
140         query = self.pool.get('account.move.line')._query_get(cr, uid, context=context)
141         cr.execute(('SELECT pid AS partner_id, SUM(bal2) FROM ' \
142                     '(SELECT CASE WHEN bal IS NOT NULL THEN bal ' \
143                     'ELSE 0.0 END AS bal2, p.id as pid FROM ' \
144                     '(SELECT (debit-credit) AS bal, partner_id ' \
145                     'FROM account_move_line l ' \
146                     'WHERE account_id IN ' \
147                             '(SELECT id FROM account_account '\
148                             'WHERE type=%s AND active) ' \
149                     'AND reconcile_id IS NULL ' \
150                     'AND '+query+') AS l ' \
151                     'RIGHT JOIN res_partner p ' \
152                     'ON p.id = partner_id ) AS pl ' \
153                     'GROUP BY pid HAVING ' + where), 
154                     (type,) + having_values)
155         res = cr.fetchall()
156         if not res:
157             return [('id','=','0')]
158         return [('id','in',map(itemgetter(0), res))]
159
160     def _credit_search(self, cr, uid, obj, name, args, context=None):
161         return self._asset_difference_search(cr, uid, obj, name, 'receivable', args, context=context)
162
163     def _debit_search(self, cr, uid, obj, name, args, context=None):
164         return self._asset_difference_search(cr, uid, obj, name, 'payable', args, context=context)
165
166     def has_something_to_reconcile(self, cr, uid, partner_id, context=None):
167         '''
168         at least a debit, a credit and a line older than the last reconciliation date of the partner
169         '''
170         cr.execute('''
171             SELECT l.partner_id, SUM(l.debit) AS debit, SUM(l.credit) AS credit
172             FROM account_move_line l
173             RIGHT JOIN account_account a ON (a.id = l.account_id)
174             RIGHT JOIN res_partner p ON (l.partner_id = p.id)
175             WHERE a.reconcile IS TRUE
176             AND p.id = %s
177             AND l.reconcile_id IS NULL
178             AND (p.last_reconciliation_date IS NULL OR l.date > p.last_reconciliation_date)
179             AND l.state <> 'draft'
180             GROUP BY l.partner_id''', (partner_id,))
181         res = cr.dictfetchone()
182         if res:
183             return bool(res['debit'] and res['credit'])
184         return False
185
186     def mark_as_reconciled(self, cr, uid, ids, context=None):
187         return self.write(cr, uid, ids, {'last_reconciliation_date': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
188
189     _columns = {
190         'credit': fields.function(_credit_debit_get,
191             fnct_search=_credit_search, string='Total Receivable', multi='dc', help="Total amount this customer owes you."),
192         'debit': fields.function(_credit_debit_get, fnct_search=_debit_search, string='Total Payable', multi='dc', help="Total amount you have to pay to this supplier."),
193         'debit_limit': fields.float('Payable Limit'),
194         'property_account_payable': fields.property(
195             'account.account',
196             type='many2one',
197             relation='account.account',
198             string="Account Payable",
199             view_load=True,
200             domain="[('type', '=', 'payable')]",
201             help="This account will be used instead of the default one as the payable account for the current partner",
202             required=True),
203         'property_account_receivable': fields.property(
204             'account.account',
205             type='many2one',
206             relation='account.account',
207             string="Account Receivable",
208             view_load=True,
209             domain="[('type', '=', 'receivable')]",
210             help="This account will be used instead of the default one as the receivable account for the current partner",
211             required=True),
212         'property_account_position': fields.property(
213             'account.fiscal.position',
214             type='many2one',
215             relation='account.fiscal.position',
216             string="Fiscal Position",
217             view_load=True,
218             help="The fiscal position will determine taxes and accounts used for the partner.",
219         ),
220         'property_payment_term': fields.property(
221             'account.payment.term',
222             type='many2one',
223             relation='account.payment.term',
224             string ='Customer Payment Terms',
225             view_load=True,
226             help="This payment term will be used instead of the default one for sale orders and customer invoices"),
227         'property_supplier_payment_term': fields.property(
228             'account.payment.term',
229              type='many2one',
230              relation='account.payment.term',
231              string ='Supplier Payment Terms',
232              view_load=True,
233              help="This payment term will be used instead of the default one for purchase orders and supplier invoices"),
234         'ref_companies': fields.one2many('res.company', 'partner_id',
235             'Companies that refers to partner'),
236         'last_reconciliation_date': fields.datetime('Latest Reconciliation Date', help='Date on which the partner accounting entries were fully reconciled last time. It differs from the date of the last reconciliation made for this partner, as here we depict the fact that nothing more was to be reconciled at this date. This can be achieved in 2 ways: either the last debit/credit entry was reconciled, either the user pressed the button "Fully Reconciled" in the manual reconciliation process')
237     }
238
239 res_partner()
240
241 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: