[MERGE] account journal items widget by dle
[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 from osv import fields, osv
24 import time
25
26 class account_fiscal_position(osv.osv):
27     _name = 'account.fiscal.position'
28     _description = 'Fiscal Position'
29     _columns = {
30         'name': fields.char('Fiscal Position', size=64, required=True),
31         'active': fields.boolean('Active', help="By unchecking the active field, you may hide a fiscal position without deleting it."),
32         'company_id': fields.many2one('res.company', 'Company'),
33         'account_ids': fields.one2many('account.fiscal.position.account', 'position_id', 'Account Mapping'),
34         'tax_ids': fields.one2many('account.fiscal.position.tax', 'position_id', 'Tax Mapping'),
35         'note': fields.text('Notes', translate=True),
36     }
37
38     _defaults = {
39         'active': True,
40     }
41
42     def map_tax(self, cr, uid, fposition_id, taxes, context=None):
43         if not taxes:
44             return []
45         if not fposition_id:
46             return map(lambda x: x.id, taxes)
47         result = set()
48         for t in taxes:
49             ok = False
50             for tax in fposition_id.tax_ids:
51                 if tax.tax_src_id.id == t.id:
52                     if tax.tax_dest_id:
53                         result.add(tax.tax_dest_id.id)
54                     ok=True
55             if not ok:
56                 result.add(t.id)
57         return list(result)
58
59     def map_account(self, cr, uid, fposition_id, account_id, context=None):
60         if not fposition_id:
61             return account_id
62         for pos in fposition_id.account_ids:
63             if pos.account_src_id.id == account_id:
64                 account_id = pos.account_dest_id.id
65                 break
66         return account_id
67
68 account_fiscal_position()
69
70 class account_fiscal_position_tax(osv.osv):
71     _name = 'account.fiscal.position.tax'
72     _description = 'Taxes Fiscal Position'
73     _rec_name = 'position_id'
74     _columns = {
75         'position_id': fields.many2one('account.fiscal.position', 'Fiscal Position', required=True, ondelete='cascade'),
76         'tax_src_id': fields.many2one('account.tax', 'Tax Source', required=True),
77         'tax_dest_id': fields.many2one('account.tax', 'Replacement Tax')
78     }
79
80     _sql_constraints = [
81         ('tax_src_dest_uniq',
82          'unique (position_id,tax_src_id,tax_dest_id)',
83          'A tax fiscal position could be defined only once time on same taxes.')
84     ]
85
86 account_fiscal_position_tax()
87
88 class account_fiscal_position_account(osv.osv):
89     _name = 'account.fiscal.position.account'
90     _description = 'Accounts Fiscal Position'
91     _rec_name = 'position_id'
92     _columns = {
93         'position_id': fields.many2one('account.fiscal.position', 'Fiscal Position', required=True, ondelete='cascade'),
94         'account_src_id': fields.many2one('account.account', 'Account Source', domain=[('type','<>','view')], required=True),
95         'account_dest_id': fields.many2one('account.account', 'Account Destination', domain=[('type','<>','view')], required=True)
96     }
97
98     _sql_constraints = [
99         ('account_src_dest_uniq',
100          'unique (position_id,account_src_id,account_dest_id)',
101          'An account fiscal position could be defined only once time on same accounts.')
102     ]
103
104 account_fiscal_position_account()
105
106 class res_partner(osv.osv):
107     _name = 'res.partner'
108     _inherit = 'res.partner'
109     _description = 'Partner'
110
111     def _credit_debit_get(self, cr, uid, ids, field_names, arg, context=None):
112         query = self.pool.get('account.move.line')._query_get(cr, uid, context=context)
113         cr.execute("""SELECT l.partner_id, a.type, SUM(l.debit-l.credit)
114                       FROM account_move_line l
115                       LEFT JOIN account_account a ON (l.account_id=a.id)
116                       WHERE a.type IN ('receivable','payable')
117                       AND l.partner_id IN %s
118                       AND l.reconcile_id IS NULL
119                       AND """ + query + """
120                       GROUP BY l.partner_id, a.type
121                       """,
122                    (tuple(ids),))
123         maps = {'receivable':'credit', 'payable':'debit' }
124         res = {}
125         for id in ids:
126             res[id] = {}.fromkeys(field_names, 0)
127         for pid,type,val in cr.fetchall():
128             if val is None: val=0
129             res[pid][maps[type]] = (type=='receivable') and val or -val
130         return res
131
132     def _asset_difference_search(self, cr, uid, obj, name, type, args, context=None):
133         if not args:
134             return []
135         having_values = tuple(map(itemgetter(2), args))
136         where = ' AND '.join(
137             map(lambda x: '(SUM(debit-credit) %(operator)s %%s)' % {
138                                 'operator':x[1]},args))
139         query = self.pool.get('account.move.line')._query_get(cr, uid, context=context)
140         cr.execute(('SELECT partner_id FROM account_move_line l '\
141                     'WHERE account_id IN '\
142                         '(SELECT id FROM account_account '\
143                         'WHERE type=%s AND active) '\
144                     'AND reconcile_id IS NULL '\
145                     'AND '+query+' '\
146                     'AND partner_id IS NOT NULL '\
147                     'GROUP BY partner_id HAVING '+where),
148                    (type,) + having_values)
149         res = cr.fetchall()
150         if not res:
151             return [('id','=','0')]
152         return [('id','in',map(itemgetter(0), res))]
153
154     def _credit_search(self, cr, uid, obj, name, args, context=None):
155         return self._asset_difference_search(cr, uid, obj, name, 'receivable', args, context=context)
156
157     def _debit_search(self, cr, uid, obj, name, args, context=None):
158         return self._asset_difference_search(cr, uid, obj, name, 'payable', args, context=context)
159
160     def has_something_to_reconcile(self, cr, uid, partner_id, context=None):
161         '''
162         at least a debit, a credit and a line older than the last reconciliation date of the partner
163         '''
164         cr.execute('''
165             SELECT l.partner_id, SUM(l.debit) AS debit, SUM(l.credit) AS credit
166             FROM account_move_line l
167             RIGHT JOIN account_account a ON (a.id = l.account_id)
168             RIGHT JOIN res_partner p ON (l.partner_id = p.id)
169             WHERE a.reconcile IS TRUE
170             AND p.id = %s
171             AND l.reconcile_id IS NULL
172             AND (p.last_reconciliation_date IS NULL OR l.date > p.last_reconciliation_date)
173             AND l.state <> 'draft'
174             GROUP BY l.partner_id''', (partner_id,))
175         res = cr.dictfetchone()
176         if res:
177             return bool(res['debit'] and res['credit'])
178         return False
179
180     def mark_as_reconciled(self, cr, uid, ids, context=None):
181         return self.write(cr, uid, ids, {'last_reconciliation_date': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
182
183     _columns = {
184         'credit': fields.function(_credit_debit_get,
185             fnct_search=_credit_search, string='Total Receivable', multi='dc', help="Total amount this customer owes you."),
186         '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."),
187         'debit_limit': fields.float('Payable Limit'),
188         'property_account_payable': fields.property(
189             'account.account',
190             type='many2one',
191             relation='account.account',
192             string="Account Payable",
193             view_load=True,
194             domain="[('type', '=', 'payable')]",
195             help="This account will be used instead of the default one as the payable account for the current partner",
196             required=True),
197         'property_account_receivable': fields.property(
198             'account.account',
199             type='many2one',
200             relation='account.account',
201             string="Account Receivable",
202             view_load=True,
203             domain="[('type', '=', 'receivable')]",
204             help="This account will be used instead of the default one as the receivable account for the current partner",
205             required=True),
206         'property_account_position': fields.property(
207             'account.fiscal.position',
208             type='many2one',
209             relation='account.fiscal.position',
210             string="Fiscal Position",
211             view_load=True,
212             help="The fiscal position will determine taxes and accounts used for the partner.",
213         ),
214         'property_payment_term': fields.property(
215             'account.payment.term',
216             type='many2one',
217             relation='account.payment.term',
218             string ='Payment Term',
219             view_load=True,
220             help="This payment term will be used instead of the default one for the current partner"),
221         'ref_companies': fields.one2many('res.company', 'partner_id',
222             'Companies that refers to partner'),
223         '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')
224     }
225
226 res_partner()
227
228 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: