[MERGE] [FIX] models: do not drop low level columns (aka 'magic columns') when deleti...
[odoo/odoo.git] / addons / account / edi / invoice.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Business Applications
5 #    Copyright (c) 2011-2012 OpenERP S.A. <http://openerp.com>
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 from openerp.osv import osv, fields
22 from openerp.addons.edi import EDIMixin
23
24 from urllib import urlencode
25
26 INVOICE_LINE_EDI_STRUCT = {
27     'name': True,
28     'origin': True,
29     'uos_id': True,
30     'product_id': True,
31     'price_unit': True,
32     'quantity': True,
33     'discount': True,
34
35     # fields used for web preview only - discarded on import
36     'price_subtotal': True,
37 }
38
39 INVOICE_TAX_LINE_EDI_STRUCT = {
40     'name': True,
41     'base': True,
42     'amount': True,
43     'manual': True,
44     'sequence': True,
45     'base_amount': True,
46     'tax_amount': True,
47 }
48
49 INVOICE_EDI_STRUCT = {
50     'name': True,
51     'origin': True,
52     'company_id': True, # -> to be changed into partner
53     'type': True, # -> reversed at import
54     'internal_number': True, # -> reference at import
55     'comment': True,
56     'date_invoice': True,
57     'date_due': True,
58     'partner_id': True,
59     'payment_term': True,
60     #custom: currency_id
61     'invoice_line': INVOICE_LINE_EDI_STRUCT,
62     'tax_line': INVOICE_TAX_LINE_EDI_STRUCT,
63
64     # fields used for web preview only - discarded on import
65     #custom: 'partner_ref'
66     'amount_total': True,
67     'amount_untaxed': True,
68     'amount_tax': True,
69 }
70
71 class account_invoice(osv.osv, EDIMixin):
72     _inherit = 'account.invoice'
73
74     def edi_export(self, cr, uid, records, edi_struct=None, context=None):
75         """Exports a supplier or customer invoice"""
76         edi_struct = dict(edi_struct or INVOICE_EDI_STRUCT)
77         res_company = self.pool.get('res.company')
78         res_partner = self.pool.get('res.partner')
79         edi_doc_list = []
80         for invoice in records:
81             # generate the main report
82             self._edi_generate_report_attachment(cr, uid, invoice, context=context)
83             edi_doc = super(account_invoice,self).edi_export(cr, uid, [invoice], edi_struct, context)[0]
84             edi_doc.update({
85                     'company_address': res_company.edi_export_address(cr, uid, invoice.company_id, context=context),
86                     'company_paypal_account': invoice.company_id.paypal_account,
87                     'partner_address': res_partner.edi_export(cr, uid, [invoice.partner_id], context=context)[0],
88                     'currency': self.pool.get('res.currency').edi_export(cr, uid, [invoice.currency_id], context=context)[0],
89                     'partner_ref': invoice.reference or False,
90             })
91             edi_doc_list.append(edi_doc)
92         return edi_doc_list
93
94     def _edi_tax_account(self, cr, uid, invoice_type='out_invoice', context=None):
95         #TODO/FIXME: should select proper Tax Account
96         account_pool = self.pool.get('account.account')
97         account_ids = account_pool.search(cr, uid, [('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')])
98         tax_account = False
99         if account_ids:
100             tax_account = account_pool.browse(cr, uid, account_ids[0])
101         return tax_account
102
103     def _edi_invoice_account(self, cr, uid, partner_id, invoice_type, context=None):
104         res_partner = self.pool.get('res.partner')
105         partner = res_partner.browse(cr, uid, partner_id, context=context)
106         if invoice_type in ('out_invoice', 'out_refund'):
107             invoice_account = partner.property_account_receivable
108         else:
109             invoice_account = partner.property_account_payable
110         return invoice_account
111
112     def _edi_product_account(self, cr, uid, product_id, invoice_type, context=None):
113         product_pool = self.pool.get('product.product')
114         product = product_pool.browse(cr, uid, product_id, context=context)
115         if invoice_type in ('out_invoice','out_refund'):
116             account = product.property_account_income or product.categ_id.property_account_income_categ
117         else:
118             account = product.property_account_expense or product.categ_id.property_account_expense_categ
119         return account
120
121     def _edi_import_company(self, cr, uid, edi_document, context=None):
122         # TODO: for multi-company setups, we currently import the document in the
123         #       user's current company, but we should perhaps foresee a way to select
124         #       the desired company among the user's allowed companies
125
126         self._edi_requires_attributes(('company_id','company_address','type'), edi_document)
127         res_partner = self.pool.get('res.partner')
128
129         xid, company_name = edi_document.pop('company_id')
130         # Retrofit address info into a unified partner info (changed in v7 - used to keep them separate)
131         company_address_edi = edi_document.pop('company_address')
132         company_address_edi['name'] = company_name
133         company_address_edi['is_company'] = True
134         company_address_edi['__import_model'] = 'res.partner'
135         company_address_edi['__id'] = xid  # override address ID, as of v7 they should be the same anyway
136         if company_address_edi.get('logo'):
137             company_address_edi['image'] = company_address_edi.pop('logo')
138
139         invoice_type = edi_document['type']
140         if invoice_type.startswith('out_'):
141             company_address_edi['customer'] = True
142         else:
143             company_address_edi['supplier'] = True
144         partner_id = res_partner.edi_import(cr, uid, company_address_edi, context=context)
145
146         # modify edi_document to refer to new partner
147         partner = res_partner.browse(cr, uid, partner_id, context=context)
148         partner_edi_m2o = self.edi_m2o(cr, uid, partner, context=context)
149         edi_document['partner_id'] = partner_edi_m2o
150         edi_document.pop('partner_address', None) # ignored, that's supposed to be our own address!
151
152         return partner_id
153
154     def edi_import(self, cr, uid, edi_document, context=None):
155         """ During import, invoices will import the company that is provided in the invoice as
156             a new partner (e.g. supplier company for a customer invoice will be come a supplier
157             record for the new invoice.
158             Summary of tasks that need to be done:
159                 - import company as a new partner, if type==in then supplier=1, else customer=1
160                 - partner_id field is modified to point to the new partner
161                 - company_address data used to add address to new partner
162                 - change type: out_invoice'<->'in_invoice','out_refund'<->'in_refund'
163                 - reference: should contain the value of the 'internal_number'
164                 - reference_type: 'none'
165                 - internal number: reset to False, auto-generated
166                 - journal_id: should be selected based on type: simply put the 'type'
167                     in the context when calling create(), will be selected correctly
168                 - payment_term: if set, create a default one based on name...
169                 - for invoice lines, the account_id value should be taken from the
170                     product's default, i.e. from the default category, as it will not
171                     be provided.
172                 - for tax lines, we disconnect from the invoice.line, so all tax lines
173                     will be of type 'manual', and default accounts should be picked based
174                     on the tax config of the DB where it is imported.
175         """
176         if context is None:
177             context = {}
178         self._edi_requires_attributes(('company_id','company_address','type','invoice_line','currency'), edi_document)
179
180         # extract currency info
181         res_currency = self.pool.get('res.currency')
182         currency_info = edi_document.pop('currency')
183         currency_id = res_currency.edi_import(cr, uid, currency_info, context=context)
184         currency = res_currency.browse(cr, uid, currency_id)
185         edi_document['currency_id'] =  self.edi_m2o(cr, uid, currency, context=context)
186
187         # change type: out_invoice'<->'in_invoice','out_refund'<->'in_refund'
188         invoice_type = edi_document['type']
189         invoice_type = invoice_type.startswith('in_') and invoice_type.replace('in_','out_') or invoice_type.replace('out_','in_')
190         edi_document['type'] = invoice_type
191
192         # import company as a new partner
193         partner_id = self._edi_import_company(cr, uid, edi_document, context=context)
194
195         # Set Account
196         invoice_account = self._edi_invoice_account(cr, uid, partner_id, invoice_type, context=context)
197         edi_document['account_id'] = invoice_account and self.edi_m2o(cr, uid, invoice_account, context=context) or False
198
199         # reference: should contain the value of the 'internal_number'
200         edi_document['reference'] = edi_document.get('internal_number', False)
201         # reference_type: 'none'
202         edi_document['reference_type'] = 'none'
203
204         # internal number: reset to False, auto-generated
205         edi_document['internal_number'] = False
206
207         # discard web preview fields, if present
208         edi_document.pop('partner_ref', None)
209
210         # journal_id: should be selected based on type: simply put the 'type' in the context when calling create(), will be selected correctly
211         context.update(type=invoice_type)
212
213         # for invoice lines, the account_id value should be taken from the product's default, i.e. from the default category, as it will not be provided.
214         for edi_invoice_line in edi_document['invoice_line']:
215             product_info = edi_invoice_line['product_id']
216             product_id = self.edi_import_relation(cr, uid, 'product.product', product_info[1],
217                                                   product_info[0], context=context)
218             account = self._edi_product_account(cr, uid, product_id, invoice_type, context=context)
219             # TODO: could be improved with fiscal positions perhaps
220             # account = fpos_obj.map_account(cr, uid, fiscal_position_id, account.id)
221             edi_invoice_line['account_id'] = self.edi_m2o(cr, uid, account, context=context) if account else False
222
223             # discard web preview fields, if present
224             edi_invoice_line.pop('price_subtotal', None)
225
226         # for tax lines, we disconnect from the invoice.line, so all tax lines will be of type 'manual', and default accounts should be picked based
227         # on the tax config of the DB where it is imported.
228         tax_account = self._edi_tax_account(cr, uid, context=context)
229         tax_account_info = self.edi_m2o(cr, uid, tax_account, context=context)
230         for edi_tax_line in edi_document.get('tax_line', []):
231             edi_tax_line['account_id'] = tax_account_info
232             edi_tax_line['manual'] = True
233
234         return super(account_invoice,self).edi_import(cr, uid, edi_document, context=context)
235
236
237     def _edi_record_display_action(self, cr, uid, id, context=None):
238         """Returns an appropriate action definition dict for displaying
239            the record with ID ``rec_id``.
240
241            :param int id: database ID of record to display
242            :return: action definition dict
243         """
244         action = super(account_invoice,self)._edi_record_display_action(cr, uid, id, context=context)
245         try:
246             invoice = self.browse(cr, uid, id, context=context)
247             if 'out_' in invoice.type:
248                 view_ext_id = 'invoice_form'
249                 journal_type = 'sale'
250             else:
251                 view_ext_id = 'invoice_supplier_form'
252                 journal_type = 'purchase'
253             ctx = "{'type': '%s', 'journal_type': '%s'}" % (invoice.type, journal_type)
254             action.update(context=ctx)
255             view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', view_ext_id)[1]
256             action.update(views=[(view_id,'form'), (False, 'tree')])
257         except ValueError:
258             # ignore if views are missing
259             pass
260         return action
261
262     def _edi_paypal_url(self, cr, uid, ids, field, arg, context=None):
263         res = dict.fromkeys(ids, False)
264         for inv in self.browse(cr, uid, ids, context=context):
265             if inv.type == 'out_invoice' and inv.company_id.paypal_account:
266                 params = {
267                     "cmd": "_xclick",
268                     "business": inv.company_id.paypal_account,
269                     "item_name": "%s Invoice %s" % (inv.company_id.name, inv.number or ''),
270                     "invoice": inv.number,
271                     "amount": inv.residual,
272                     "currency_code": inv.currency_id.name,
273                     "button_subtype": "services",
274                     "no_note": "1",
275                     "bn": "OpenERP_Invoice_PayNow_" + inv.currency_id.name,
276                 }
277                 res[inv.id] = "https://www.paypal.com/cgi-bin/webscr?" + urlencode(params)
278         return res
279
280     _columns = {
281         'paypal_url': fields.function(_edi_paypal_url, type='char', string='Paypal Url'),
282     }
283
284
285 class account_invoice_line(osv.osv, EDIMixin):
286     _inherit='account.invoice.line'
287
288 class account_invoice_tax(osv.osv, EDIMixin):
289     _inherit = "account.invoice.tax"
290
291
292
293 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: