-
Notifications
You must be signed in to change notification settings - Fork 2
/
openresa_invoicing.py
394 lines (359 loc) · 21 KB
/
openresa_invoicing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# -*- coding: utf-8 -*-
##############################################################################
# Copyright (C) 2012 SICLIC http://siclic.fr
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
#############################################################################
from osv import fields, osv
import netsvc
import time
import base64
import re
from datetime import datetime,timedelta
from datetime import datetime
from mx.DateTime.mxDateTime import strptime
from openbase.openbase_core import OpenbaseCore
class sale_order(OpenbaseCore):
_inherit = "sale.order"
_name = "sale.order"
"""
@param record: browse_record to create report
@return: id of created ir.attachment, using first ir.actions.report.xml found for this object
"""
def _create_report_attach(self, cr, uid, record, context=None):
#sources insipered by _edi_generate_report_attachment of EDIMIXIN module
ir_actions_report = self.pool.get('ir.actions.report.xml')
matching_reports = ir_actions_report.search(cr, uid, [('model','=',self._name),
('report_type','=','pdf')])
ret = False
if matching_reports:
report = ir_actions_report.browse(cr, uid, matching_reports[0])
report_service = 'report.' + report.report_name
service = netsvc.LocalService(report_service)
(result, format) = service.create(cr, uid, [record.id], {'model': self._name}, context=context)
eval_context = {'time': time, 'object': record}
if not report.attachment or not eval(report.attachment, eval_context):
# no auto-saving of report as attachment, need to do it manually
result = base64.b64encode(result)
file_name = record.name_get()[0][1]
file_name = re.sub(r'[^a-zA-Z0-9_-]', '_', file_name)
file_name += ".pdf"
ir_attachment = self.pool.get('ir.attachment').create(cr, uid,
{'name': file_name,
'datas': result,
'datas_fname': file_name,
'res_model': self._name,
'res_id': record.id},
context=context)
ret = ir_attachment
return ret
sale_order()
class account_invoice(OpenbaseCore):
_inherit = "account.invoice"
_name = "account.invoice"
_columns = {
}
"""
@param record: browse_record to create report
@return: id of created ir.attachment, using first ir.actions.report.xml found for this object
"""
def _create_report_attach(self, cr, uid, record, context=None):
#sources insipered by _edi_generate_report_attachment of EDIMIXIN module
ir_actions_report = self.pool.get('ir.actions.report.xml')
matching_reports = ir_actions_report.search(cr, uid, [('model','=',self._name),
('report_type','=','pdf')])
ret = False
if matching_reports:
report = ir_actions_report.browse(cr, uid, matching_reports[0])
report_service = 'report.' + report.report_name
service = netsvc.LocalService(report_service)
(result, format) = service.create(cr, uid, [record.id], {'model': self._name}, context=context)
eval_context = {'time': time, 'object': record}
if not report.attachment or not eval(report.attachment, eval_context):
# no auto-saving of report as attachment, need to do it manually
result = base64.b64encode(result)
file_name = record.name_get()[0][1]
file_name = re.sub(r'[^a-zA-Z0-9_-]', '_', file_name)
file_name += ".pdf"
ir_attachment = self.pool.get('ir.attachment').create(cr, uid,
{'name': file_name,
'datas': result,
'datas_fname': file_name,
'res_model': self._name,
'res_id': record.id},
context=context)
ret = ir_attachment
return ret
""" @note: override to force creation of pdf report (base function (ir.actions.server) was unlinked and replaced by this one)"""
def action_number(self, cr, uid, ids, context=None):
res = super(account_invoice, self).action_number(cr, uid, ids, context)
for inv in self.browse(cr, uid, ids, context):
report_attach = self._create_report_attach(cr, uid, inv, context)
return res
account_invoice()
class purchase_order(OpenbaseCore):
_inherit = "purchase.order"
_name = "purchase.order"
_columns = {'is_emprunt':fields.boolean('Demande d\'emprunt', help="Indique qu'il s'agit d'une demande d'emprunt aurpès d'une mairie extèrieure et non d'un bon de commande")}
_defaults = {
'is_emprunt':lambda *a: 0,
}
""" @note: OpenERP action button method to end-up this purchase (correspond to a booking by this company to another, to complete bookable stock)
@deprecated: the feature with this method is deprecated, to remove"""
def emprunt_done(self, cr, uid, ids):
self.write(cr, uid, ids, {'state':'done'})
return True
"""@note: Force purchase.order workflow to cancel its pickings (subflow returns cancel and reactivate workitem at picking activity)"""
def do_terminate_emprunt(self, cr, uid, ids, context=None):
list_picking_ids = []
wf_service = netsvc.LocalService('workflow')
for purchase in self.browse(cr, uid, ids):
for picking in purchase.picking_ids:
wf_service.trg_validate(uid, 'stock.picking', picking.id, 'button_cancel', cr)
wf_service.trg_write(uid, 'purchase.order', purchase.id, cr)
return {
'res_model':'purchase.order',
'type:':'ir.actions.act_window',
'view_mode':'form',
'target':'current',
}
purchase_order()
class hotel_reservation_line(OpenbaseCore):
_inherit = "hotel_reservation.line"
"""OpenERP functionnal field method, compute amount using qte_reserves and pricelist_amount
(assuming there is no additionnal element, such as taxes, to modifiy this value) """
def _get_amount(self, cr, uid, ids, name, args, context=None):
ret = {}.fromkeys(ids, 0.0)
for line in self.browse(cr, uid, ids, context):
amount = line.pricelist_amount * line.qte_reserves
ret.update({line.id:amount})
#TOCHECK: is there any taxe when collectivity invoice people ?
return ret
_columns = {
'pricelist_amount':fields.float('Price from pricelist'),
'pricelist_item':fields.many2one('product.pricelist.item','Pricelist item of invoicing'),
'uom_qty':fields.float('Qté de Référence pour Facturation',digit=(2,1)),
'amount':fields.function(_get_amount, string="Prix (si tarifé)", type="float", method=True, store=False),
}
hotel_reservation_line()
class hotel_reservation(OpenbaseCore):
_inherit = "hotel.reservation"
"""
@param record: browse_record of hotel.reservation for which to generate hotel.folio report
@return: id or attachment created for this record
@note: hotel.folio report is created on hotel.reservation because hotel.folio has not any form view for now
"""
def _create_report_folio_attach(self, cr, uid, record, context=None):
#sources insipered by _edi_generate_report_attachment of EDIMIXIN module
ir_actions_report = self.pool.get('ir.actions.report.xml')
report_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'openresa','folio_report')[1]
ret = False
if report_id:
report = ir_actions_report.browse(cr, uid, report_id,context=context)
report_service = 'report.' + report.report_name
service = netsvc.LocalService(report_service)
(result, format) = service.create(cr, uid, [folio.id for folio in record.folio_id], {'model': self._name}, context=context)
eval_context = {'time': time, 'object': record}
# no auto-saving of report as attachment, need to do it manually
result = base64.b64encode(result)
file_name = 'Facturation_' + record.reservation_no
file_name = re.sub(r'[^a-zA-Z0-9_-]', '_', file_name)
file_name += ".pdf"
ir_attachment = self.pool.get('ir.attachment').create(cr, uid,
{'name': file_name,
'datas': result,
'datas_fname': file_name,
'res_model': self._name,
'res_id': record.id},
context=context)
ret = ir_attachment
record.write({'invoice_attachment_id': ret})
return ret
def _get_amount_total(self, cr, uid, ids, name, args, context=None):
ret = {}
for resa in self.browse(cr, uid, ids, context=None):
amount_total = 0.0
all_dispo = True
for line in resa.reservation_line:
if all_dispo and not line.dispo:
all_dispo = False
amount_total += line.amount
ret[resa.id] = {'amount_total':amount_total,'all_dispo':all_dispo}
return ret
_columns = {
'amount_total': fields.function(_get_amount_total, type='float', string='Amount Total', method=True,
multi="resa",
help='Optionnal, if positive, a sale order will be created once resa validated and invoice will be created once resa done.'),
'all_dispo': fields.function(_get_amount_total, type="boolean", string="All Dispo", method=True, multi="resa"),
'send_invoicing': fields.boolean('Send invoicing by email'),
'invoice_attachment_id': fields.integer('Attachment ID'),
'send_email':fields.boolean('Send Email', help='If set, authorize sending email until its unchecked'),
}
"""polymorphism of _create_folio
@note: manage both individual resa and recurrente resa (1 line = 1 occurrence)
"""
def create_folio(self, cr, uid, ids, context=None):
for reservation in self.browse(cr,uid,ids):
#first, if it is a recurrence, get all occurrences to generate folio, else, keep only current resa
lines = []
#checkin and checkout are used to set the highest scale of dates of a recurrence
checkin = False
checkout = False
if reservation.recurrence_id:
for resa in reservation.recurrence_id.reservation_ids:
lines.extend([line for line in resa.reservation_line if resa.all_dispo])
#retrieve min and max date for all the recurrence
checkin = resa.checkin if not checkin else min(resa.checkin, checkin)
checkout = resa.checkout if not checkout else max(resa.checkout, checkout)
else:
lines.extend(line for line in reservation.reservation_line)
checkin = reservation.checkin
checkout = reservation.checkout
room_lines = []
for line in lines:
room_lines.append((0,0,{
'checkin_date':line.line_id.checkin,
'checkout_date':line.line_id.checkout,
'product_id':line.reserve_product.id,
'name':line.reserve_product.name,
'product_uom':line.reserve_product.uom_id.id,
'product_uom_qty':line.qte_reserves,
'product_uos':line.reserve_product.uos_id and line.reserve_product.uos_id.id or line.reserve_product.uom_id.id,
'product_uos_qty':line.uom_qty,
'price_unit':line.pricelist_amount,
}))
#if resa is from on recurrence, copy all room_lines for each resa (update checkin and checkout for each one)
folio=self.pool.get('hotel.folio').create(cr,uid,{
'date_order':reservation.date_order,
'shop_id':reservation.shop_id.id,
'partner_id':reservation.partner_id.id,
'pricelist_id':reservation.pricelist_id.id,
'partner_invoice_id':reservation.partner_invoice_id.id,
'partner_order_id':reservation.partner_order_id.id,
'partner_shipping_id':reservation.partner_shipping_id.id,
'checkin_date': checkin,
'checkout_date': checkout,
'room_lines':room_lines,
})
#TODO: check useless ?
cr.execute('insert into hotel_folio_reservation_rel (order_id,invoice_id) values (%s,%s)', (reservation.id, folio))
return folio
"""
@param checkin: datetime start of booking
@param checkout: datetime end of booking
@return: length of booking in hour (hour is the default uom for booking pricing)
"""
def get_length_resa(self, cr, uid, checkin, checkout, context=None):
checkin = strptime(checkin, '%Y-%m-%d %H:%M:%S')
checkout = strptime(checkout, '%Y-%m-%d %H:%M:%S')
length = (checkout - checkin).hours
return length
"""
@param product_id: id of bookable to retrieve pricing
@param uom_qty: qty computed by get_length_resa used to get pricelist_item
@param partner_id: id of claimer, used to retrieve pricelist_item
@param pricelist_id: optional param to replace default partner pricelist
@return: pricing of booking (float) if found, else False
"""
def get_prod_price(self, cr, uid, product_id, uom_qty, partner_id, pricelist_id=False, context=None):
pricelist_obj = self.pool.get("product.pricelist")
if not pricelist_id:
pricelist_id = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).property_product_pricelist.id
res = pricelist_obj.price_get_multi(cr, uid, [pricelist_id], [(product_id,uom_qty,partner_id)], context=None)
return res and (res[product_id][pricelist_id]) or False
"""
@note: OpenERP internal invoicing compute, retrieve uom_qty (based on booking_length in hour and bookable.uom_id)
and pricing (using pricelist filled in booking, default is partner-pricelist)
"""
def compute_lines_price(self, cr, uid, ids, context=None):
values = []
api_group = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'openresa', 'openresa_api_group')[1]
user_is_api = api_group in self.pool.get('res.users').read(cr, uid, uid, ['groups_id'])['groups_id']
#get lentgh resa in hours
for resa in self.browse(cr, uid, ids, context):
partner_id = resa.partner_id.id
pricelist_id = resa.pricelist_id and resa.pricelist_id.id or resa.partner.property_product_pricelist.id
length_resa = self.get_length_resa(cr, uid, resa.checkin, resa.checkout, context=None)
prod_obj = self.pool.get('product.product')
for line in resa.reservation_line:
if line.pricelist_amount <= 0.0 and user_is_api:
uom_qty = prod_obj.get_temporal_uom_qty(cr, uid, line.reserve_product.id, length_resa, context)
unit_price = self.get_prod_price(cr, uid, line.reserve_product.id,
uom_qty,
partner_id,
pricelist_id,
context=context)
values.append((1,line.id,{'uom_qty':uom_qty,'pricelist_amount':unit_price}))
if values:
self.write(cr, uid, [resa.id], {'reservation_line':values}, context=context)
return True
"""
@param vals: Dict containing "to" (deprecated) and "state" in ("error","waiting", "validated","done") (required)
"state" is a shortcut to retrieve template_xml_id
@param attach_ids: optionnal parameter to manually add attaches to mail
@note: send mail according to 'state' value, and send mail only for simple-booking or template-booking records
"""
def envoyer_mail(self, cr, uid, ids, vals=None, attach_ids=[], context=None):
#TODO: check if company wants to send email (info not(opt_out) in partner)
#We keep only resa where partner have not opt_out checked
resa_ids_notif = []
resa_ids_notif = [resa.id for resa in self.browse(cr, uid, ids)
if not resa.partner_id.opt_out
and (not resa.recurrence_id or resa.is_template)]
if resa_ids_notif:
email_obj = self.pool.get("email.template")
email_tmpl_id = 0
prod_attaches = {}
data_obj = self.pool.get('ir.model.data')
model_map = {'error':'openstc_pret_email_template_resa_refused',
'cancel':'openstc_pret_email_template_resa_cancelled',
'cancel_manager':'openstc_pret_email_template_resa_cancelled_manager',
'waiting':'openstc_pret_email_template_resa_en_attente',
'done':'openstc_pret_email_template_resa_done',
'deleted':'openstc_pret_email_template_resa_deleted',
'validated':'openstc_pret_email_template_resa_validee',
'modified_manager':'openstc_pret_email_template_resa_modified_manager'}
#first, retrieve template_id according to 'state' parameter
if vals.get('state','') in model_map.keys():
email_tmpl_id = data_obj.get_object_reference(cr, uid, 'openresa',model_map.get(vals.get('state')))[1]
#special behavior for confirm notifications
if vals['state'] == 'validated':
#Search for product attaches to be added to email
prod_ids = []
for resa in self.browse(cr, uid, ids):
prod_ids.extend([line.reserve_product.id for line in resa.reservation_line])
if prod_ids:
cr.execute("select id, res_id from ir_attachment where res_id in %s and res_model=%s order by res_id", (tuple(prod_ids), 'product.product'))
#format sql return to concat attaches with each prod_id
for item in cr.fetchall():
prod_attaches.setdefault(item[1],[])
prod_attaches[item[1]].append(item[0])
if email_tmpl_id:
if isinstance(email_tmpl_id, list):
email_tmpl_id = email_tmpl_id[0]
#generate mail and send it with optional attaches
for resa in self.browse(cr, uid, resa_ids_notif):
#link attaches of each product
attach_values = []
for line in resa.reservation_line:
if prod_attaches.has_key(line.reserve_product.id):
attach_values.extend([(4,attach_id) for attach_id in prod_attaches[line.reserve_product.id]])
#and link optional paramter attach_ids
attach_values.extend([(4,x) for x in attach_ids])
mail_id = email_obj.send_mail(cr, uid, email_tmpl_id, resa.id)
self.pool.get("mail.message").write(cr, uid, [mail_id], {'attachment_ids':attach_values})
self.pool.get("mail.message").send(cr, uid, [mail_id])
return True
hotel_reservation()