[IMP] : Update Description for modules
[odoo/odoo.git] / addons / point_of_sale / point_of_sale.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 import time
23 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
25 import logging
26 from PIL import Image
27
28 import netsvc
29 from osv import fields, osv
30 from tools.translate import _
31 from decimal import Decimal
32 import decimal_precision as dp
33
34 _logger = logging.getLogger(__name__)
35
36 class pos_config_journal(osv.osv):
37     """ Point of Sale journal configuration"""
38     _name = 'pos.config.journal'
39     _description = "Journal Configuration"
40
41     _columns = {
42         'name': fields.char('Description', size=64),
43         'code': fields.char('Code', size=64),
44         'journal_id': fields.many2one('account.journal', "Journal")
45     }
46
47 pos_config_journal()
48
49 class pos_order(osv.osv):
50     _name = "pos.order"
51     _description = "Point of Sale"
52     _order = "id desc"
53
54     def create_from_ui(self, cr, uid, orders, context=None):
55         #_logger.info("orders: %r", orders)
56         list = []
57         for order in orders:
58             # order :: {'name': 'Order 1329148448062', 'amount_paid': 9.42, 'lines': [[0, 0, {'discount': 0, 'price_unit': 1.46, 'product_id': 124, 'qty': 5}], [0, 0, {'discount': 0, 'price_unit': 0.53, 'product_id': 62, 'qty': 4}]], 'statement_ids': [[0, 0, {'journal_id': 7, 'amount': 9.42, 'name': '2012-02-13 15:54:12', 'account_id': 12, 'statement_id': 21}]], 'amount_tax': 0, 'amount_return': 0, 'amount_total': 9.42}
59             order_obj = self.pool.get('pos.order')
60             # get statements out of order because they will be generated with add_payment to ensure
61             # the module behavior is the same when using the front-end or the back-end
62             statement_ids = order.pop('statement_ids')
63             order_id = self.create(cr, uid, order, context)
64             list.append(order_id)
65             # call add_payment; refer to wizard/pos_payment for data structure
66             # add_payment launches the 'paid' signal to advance the workflow to the 'paid' state
67             data = {
68                 'journal': statement_ids[0][2]['journal_id'],
69                 'amount': order['amount_paid'],
70                 'payment_name': order['name'],
71                 'payment_date': statement_ids[0][2]['name'],
72             }
73             order_obj.add_payment(cr, uid, order_id, data, context=context)
74         return list
75
76     def unlink(self, cr, uid, ids, context=None):
77         for rec in self.browse(cr, uid, ids, context=context):
78             if rec.state not in ('draft','cancel'):
79                 raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.'))
80         return super(pos_order, self).unlink(cr, uid, ids, context=context)
81
82     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
83         if not part:
84             return {'value': {}}
85         pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
86         return {'value': {'pricelist_id': pricelist}}
87
88     def _amount_all(self, cr, uid, ids, name, args, context=None):
89         tax_obj = self.pool.get('account.tax')
90         cur_obj = self.pool.get('res.currency')
91         res = {}
92         for order in self.browse(cr, uid, ids, context=context):
93             res[order.id] = {
94                 'amount_paid': 0.0,
95                 'amount_return':0.0,
96                 'amount_tax':0.0,
97             }
98             val1 = val2 = 0.0
99             cur = order.pricelist_id.currency_id
100             for payment in order.statement_ids:
101                 res[order.id]['amount_paid'] +=  payment.amount
102                 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
103             for line in order.lines:
104                 val1 += line.price_subtotal_incl
105                 val2 += line.price_subtotal
106             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
107             res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
108         return res
109
110     def _default_sale_journal(self, cr, uid, context=None):
111         res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1)
112         return res and res[0] or False
113
114     def _default_shop(self, cr, uid, context=None):
115         res = self.pool.get('sale.shop').search(cr, uid, [])
116         return res and res[0] or False
117
118     def copy(self, cr, uid, id, default=None, context=None):
119         if not default:
120             default = {}
121         d = {
122             'state': 'draft',
123             'invoice_id': False,
124             'account_move': False,
125             'picking_id': False,
126             'statement_ids': [],
127             'nb_print': 0,
128             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
129         }
130         d.update(default)
131         return super(pos_order, self).copy(cr, uid, id, d, context=context)
132
133     _columns = {
134         'name': fields.char('Order Ref', size=64, required=True,
135             states={'draft': [('readonly', False)]}, readonly=True),
136         'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
137         'shop_id': fields.many2one('sale.shop', 'Shop', required=True,
138             states={'draft': [('readonly', False)]}, readonly=True),
139         'date_order': fields.datetime('Date Ordered', readonly=True, select=True),
140         'user_id': fields.many2one('res.users', 'Connected Salesman', help="Person who uses the the cash register. It could be a reliever, a student or an interim employee."),
141         'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
142         'amount_total': fields.function(_amount_all, string='Total', multi='all'),
143         'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
144         'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
145         'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
146         'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
147         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
148         'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
149
150         'state': fields.selection([('draft', 'New'),
151                                    ('cancel', 'Cancelled'),
152                                    ('paid', 'Paid'),
153                                    ('done', 'Posted'),
154                                    ('invoiced', 'Invoiced')],
155                                   'Status', readonly=True),
156
157         'invoice_id': fields.many2one('account.invoice', 'Invoice'),
158         'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
159         'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
160         'note': fields.text('Internal Notes'),
161         'nb_print': fields.integer('Number of Print', readonly=True),
162         'sale_journal': fields.many2one('account.journal', 'Journal', required=True, states={'draft': [('readonly', False)]}, readonly=True),
163     }
164
165     def _default_pricelist(self, cr, uid, context=None):
166         res = self.pool.get('sale.shop').search(cr, uid, [], context=context)
167         if res:
168             shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context)
169             return shop.pricelist_id and shop.pricelist_id.id or False
170         return False
171
172     _defaults = {
173         'user_id': lambda self, cr, uid, context: uid,
174         'state': 'draft',
175         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
176         'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
177         'nb_print': 0,
178         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
179         'sale_journal': _default_sale_journal,
180         'shop_id': _default_shop,
181         'pricelist_id': _default_pricelist,
182     }
183
184     def test_paid(self, cr, uid, ids, context=None):
185         """A Point of Sale is paid when the sum
186         @return: True
187         """
188         for order in self.browse(cr, uid, ids, context=context):
189             if order.lines and not order.amount_total:
190                 return True
191             if (not order.lines) or (not order.statement_ids) or \
192                 (abs(order.amount_total-order.amount_paid) > 0.00001):
193                 return False
194         return True
195
196     def create_picking(self, cr, uid, ids, context=None):
197         """Create a picking for each order and validate it."""
198         picking_obj = self.pool.get('stock.picking')
199         partner_obj = self.pool.get('res.partner')
200         move_obj = self.pool.get('stock.move')
201
202         for order in self.browse(cr, uid, ids, context=context):
203             if not order.state=='draft':
204                 continue
205             addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
206             picking_id = picking_obj.create(cr, uid, {
207                 'origin': order.name,
208                 'partner_id': addr.get('delivery',False),
209                 'type': 'out',
210                 'company_id': order.company_id.id,
211                 'move_type': 'direct',
212                 'note': order.note or "",
213                 'invoice_state': 'none',
214                 'auto_picking': True,
215             }, context=context)
216             self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
217             location_id = order.shop_id.warehouse_id.lot_stock_id.id
218             output_id = order.shop_id.warehouse_id.lot_output_id.id
219
220             for line in order.lines:
221                 if line.product_id and line.product_id.type == 'service':
222                     continue
223                 if line.qty < 0:
224                     location_id, output_id = output_id, location_id
225
226                 move_obj.create(cr, uid, {
227                     'name': line.name,
228                     'product_uom': line.product_id.uom_id.id,
229                     'product_uos': line.product_id.uom_id.id,
230                     'picking_id': picking_id,
231                     'product_id': line.product_id.id,
232                     'product_uos_qty': abs(line.qty),
233                     'product_qty': abs(line.qty),
234                     'tracking_id': False,
235                     'state': 'draft',
236                     'location_id': location_id,
237                     'location_dest_id': output_id,
238                 }, context=context)
239                 if line.qty < 0:
240                     location_id, output_id = output_id, location_id
241
242             wf_service = netsvc.LocalService("workflow")
243             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
244             picking_obj.force_assign(cr, uid, [picking_id], context)
245         return True
246
247     def set_to_draft(self, cr, uid, ids, *args):
248         if not len(ids):
249             return False
250         for order in self.browse(cr, uid, ids, context=context):
251             if order.state<>'cancel':
252                 raise osv.except_osv(_('Error!'), _('In order to set to draft a sale, it must be cancelled.'))
253         self.write(cr, uid, ids, {'state': 'draft'})
254         wf_service = netsvc.LocalService("workflow")
255         for i in ids:
256             wf_service.trg_create(uid, 'pos.order', i, cr)
257         return True
258
259     def cancel_order(self, cr, uid, ids, context=None):
260         """ Changes order state to cancel
261         @return: True
262         """
263         stock_picking_obj = self.pool.get('stock.picking')
264         for order in self.browse(cr, uid, ids, context=context):
265             wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr)
266             if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
267                 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
268         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
269         return True
270
271     def add_payment(self, cr, uid, order_id, data, context=None):
272         """Create a new payment for the order"""
273         statement_obj = self.pool.get('account.bank.statement')
274         statement_line_obj = self.pool.get('account.bank.statement.line')
275         prod_obj = self.pool.get('product.product')
276         property_obj = self.pool.get('ir.property')
277         curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
278         curr_company = curr_c.id
279         order = self.browse(cr, uid, order_id, context=context)
280         ids_new = []
281         args = {
282             'amount': data['amount'],
283         }
284         if 'payment_date' in data.keys():
285             args['date'] = data['payment_date']
286         args['name'] = order.name
287         if data.get('payment_name', False):
288             args['name'] = args['name'] + ': ' + data['payment_name']
289         account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
290         args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
291                              and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
292         args['partner_id'] = order.partner_id and order.partner_id.id or None
293
294         if not args['account_id']:
295             if not args['partner_id']:
296                 msg = _('There is no receivable account defined to make payment')
297             else:
298                 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d)') % (order.partner_id.name, order.partner_id.id,)
299             raise osv.except_osv(_('Configuration Error !'), msg)
300
301         statement_id = statement_obj.search(cr,uid, [
302                                                      ('journal_id', '=', int(data['journal'])),
303                                                      ('company_id', '=', curr_company),
304                                                      ('user_id', '=', uid),
305                                                      ('state', '=', 'open')], context=context)
306         if len(statement_id) == 0:
307             raise osv.except_osv(_('Error !'), _('You have to open at least one cashbox'))
308         if statement_id:
309             statement_id = statement_id[0]
310         args['statement_id'] = statement_id
311         args['pos_statement_id'] = order_id
312         args['journal_id'] = int(data['journal'])
313         args['type'] = 'customer'
314         args['ref'] = order.name
315         statement_line_obj.create(cr, uid, args, context=context)
316         ids_new.append(statement_id)
317
318         wf_service = netsvc.LocalService("workflow")
319         wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
320         wf_service.trg_write(uid, 'pos.order', order_id, cr)
321
322         return statement_id
323
324     def refund(self, cr, uid, ids, context=None):
325         """Create a copy of order  for refund order"""
326         clone_list = []
327         line_obj = self.pool.get('pos.order.line')
328         for order in self.browse(cr, uid, ids, context=context):
329             clone_id = self.copy(cr, uid, order.id, {
330                 'name': order.name + ' REFUND',
331             }, context=context)
332             clone_list.append(clone_id)
333
334         for clone in self.browse(cr, uid, clone_list, context=context):
335             for order_line in clone.lines:
336                 line_obj.write(cr, uid, [order_line.id], {
337                     'qty': -order_line.qty
338                 }, context=context)
339
340         new_order = ','.join(map(str,clone_list))
341         abs = {
342             #'domain': "[('id', 'in', ["+new_order+"])]",
343             'name': _('Return Products'),
344             'view_type': 'form',
345             'view_mode': 'form',
346             'res_model': 'pos.order',
347             'res_id':clone_list[0],
348             'view_id': False,
349             'context':context,
350             'type': 'ir.actions.act_window',
351             'nodestroy': True,
352             'target': 'current',
353         }
354         return abs
355
356     def action_invoice_state(self, cr, uid, ids, context=None):
357         return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
358
359     def action_invoice(self, cr, uid, ids, context=None):
360         wf_service = netsvc.LocalService("workflow")
361         inv_ref = self.pool.get('account.invoice')
362         inv_line_ref = self.pool.get('account.invoice.line')
363         product_obj = self.pool.get('product.product')
364         inv_ids = []
365
366         for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
367             if order.invoice_id:
368                 inv_ids.append(order.invoice_id.id)
369                 continue
370
371             if not order.partner_id:
372                 raise osv.except_osv(_('Error'), _('Please provide a partner for the sale.'))
373
374             acc = order.partner_id.property_account_receivable.id
375             inv = {
376                 'name': order.name,
377                 'origin': order.name,
378                 'account_id': acc,
379                 'journal_id': order.sale_journal.id or None,
380                 'type': 'out_invoice',
381                 'reference': order.name,
382                 'partner_id': order.partner_id.id,
383                 'comment': order.note or '',
384                 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
385             }
386             inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
387             if not inv.get('account_id', None):
388                 inv['account_id'] = acc
389             inv_id = inv_ref.create(cr, uid, inv, context=context)
390
391             self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
392             inv_ids.append(inv_id)
393             for line in order.lines:
394                 inv_line = {
395                     'invoice_id': inv_id,
396                     'product_id': line.product_id.id,
397                     'quantity': line.qty,
398                 }
399                 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
400                 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
401                                                                line.product_id.id,
402                                                                line.product_id.uom_id.id,
403                                                                line.qty, partner_id = order.partner_id.id,
404                                                                fposition_id=order.partner_id.property_account_position.id)['value'])
405                 if line.product_id.description_sale:
406                     inv_line['note'] = line.product_id.description_sale
407                 inv_line['price_unit'] = line.price_unit
408                 inv_line['discount'] = line.discount
409                 inv_line['name'] = inv_name
410                 inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
411                     and [(6, 0, inv_line['invoice_line_tax_id'])] or []
412                 inv_line_ref.create(cr, uid, inv_line, context=context)
413             inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
414             wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
415
416         if not inv_ids: return {}
417
418         mod_obj = self.pool.get('ir.model.data')
419         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
420         res_id = res and res[1] or False
421         return {
422             'name': _('Customer Invoice'),
423             'view_type': 'form',
424             'view_mode': 'form',
425             'view_id': [res_id],
426             'res_model': 'account.invoice',
427             'context': "{'type':'out_invoice'}",
428             'type': 'ir.actions.act_window',
429             'nodestroy': True,
430             'target': 'current',
431             'res_id': inv_ids and inv_ids[0] or False,
432         }
433
434     def create_account_move(self, cr, uid, ids, context=None):
435         """Create a account move line of order grouped by products or not."""
436         account_move_obj = self.pool.get('account.move')
437         account_move_line_obj = self.pool.get('account.move.line')
438         account_period_obj = self.pool.get('account.period')
439         period = account_period_obj.find(cr, uid, context=context)[0]
440         account_tax_obj = self.pool.get('account.tax')
441         res_obj=self.pool.get('res.users')
442         property_obj=self.pool.get('ir.property')
443
444         for order in self.browse(cr, uid, ids, context=context):
445             if order.state<>'paid': continue
446
447             curr_c = res_obj.browse(cr, uid, uid).company_id
448             comp_id = res_obj.browse(cr, order.user_id.id, order.user_id.id).company_id
449             comp_id = comp_id and comp_id.id or False
450             to_reconcile = []
451             group_tax = {}
452             account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
453
454             order_account = order.partner_id and order.partner_id.property_account_receivable and order.partner_id.property_account_receivable.id or account_def or curr_c.account_receivable.id
455
456             # Create an entry for the sale
457             move_id = account_move_obj.create(cr, uid, {
458                 'journal_id': order.sale_journal.id,
459             }, context=context)
460
461             # Create an move for each order line
462             for line in order.lines:
463                 tax_amount = 0
464                 taxes = [t for t in line.product_id.taxes_id]
465                 computed = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)
466                 computed_taxes = computed['taxes']
467
468                 for tax in computed_taxes:
469                     tax_amount += round(tax['amount'], 2)
470                     group_key = (tax['tax_code_id'],
471                                 tax['base_code_id'],
472                                 tax['account_collected_id'])
473
474                     if group_key in group_tax:
475                         group_tax[group_key] += round(tax['amount'], 2)
476                     else:
477                         group_tax[group_key] = round(tax['amount'], 2)
478                 amount = line.price_subtotal
479
480                 # Search for the income account
481                 if  line.product_id.property_account_income.id:
482                     income_account = line.product_id.property_account_income.id
483                 elif line.product_id.categ_id.property_account_income_categ.id:
484                     income_account = line.product_id.categ_id.property_account_income_categ.id
485                 else:
486                     raise osv.except_osv(_('Error !'), _('There is no income '\
487                         'account defined for this product: "%s" (id:%d)') \
488                         % (line.product_id.name, line.product_id.id, ))
489
490                 # Empty the tax list as long as there is no tax code:
491                 tax_code_id = False
492                 tax_amount = 0
493                 while computed_taxes:
494                     tax = computed_taxes.pop(0)
495                     if amount > 0:
496                         tax_code_id = tax['base_code_id']
497                         tax_amount = line.price_subtotal * tax['base_sign']
498                     else:
499                         tax_code_id = tax['ref_base_code_id']
500                         tax_amount = line.price_subtotal * tax['ref_base_sign']
501                     # If there is one we stop
502                     if tax_code_id:
503                         break
504
505
506                 # Create a move for the line
507                 account_move_line_obj.create(cr, uid, {
508                     'name': line.name,
509                     'date': order.date_order[:10],
510                     'ref': order.name,
511                     'quantity': line.qty,
512                     'product_id': line.product_id.id,
513                     'move_id': move_id,
514                     'account_id': income_account,
515                     'company_id': comp_id,
516                     'credit': ((amount>0) and amount) or 0.0,
517                     'debit': ((amount<0) and -amount) or 0.0,
518                     'journal_id': order.sale_journal.id,
519                     'period_id': period,
520                     'tax_code_id': tax_code_id,
521                     'tax_amount': tax_amount,
522                     'partner_id': order.partner_id and order.partner_id.id or False
523                 }, context=context)
524
525                 # For each remaining tax with a code, whe create a move line
526                 for tax in computed_taxes:
527                     if amount > 0:
528                         tax_code_id = tax['base_code_id']
529                         tax_amount = line.price_subtotal * tax['base_sign']
530                     else:
531                         tax_code_id = tax['ref_base_code_id']
532                         tax_amount = line.price_subtotal * tax['ref_base_sign']
533                     if not tax_code_id:
534                         continue
535
536                     account_move_line_obj.create(cr, uid, {
537                         'name': "Tax" + line.name,
538                         'date': order.date_order[:10],
539                         'ref': order.name,
540                         'product_id':line.product_id.id,
541                         'quantity': line.qty,
542                         'move_id': move_id,
543                         'account_id': income_account,
544                         'company_id': comp_id,
545                         'credit': 0.0,
546                         'debit': 0.0,
547                         'journal_id': order.sale_journal.id,
548                         'period_id': period,
549                         'tax_code_id': tax_code_id,
550                         'tax_amount': tax_amount,
551                     }, context=context)
552
553
554             # Create a move for each tax group
555             (tax_code_pos, base_code_pos, account_pos)= (0, 1, 2)
556             for key, amount in group_tax.items():
557                 account_move_line_obj.create(cr, uid, {
558                     'name': 'Tax',
559                     'date': order.date_order[:10],
560                     'ref': order.name,
561                     'move_id': move_id,
562                     'company_id': comp_id,
563                     'quantity': line.qty,
564                     'product_id': line.product_id.id,
565                     'account_id': key[account_pos],
566                     'credit': ((amount>0) and amount) or 0.0,
567                     'debit': ((amount<0) and -amount) or 0.0,
568                     'journal_id': order.sale_journal.id,
569                     'period_id': period,
570                     'tax_code_id': key[tax_code_pos],
571                     'tax_amount': amount,
572                 }, context=context)
573
574             # counterpart
575             to_reconcile.append(account_move_line_obj.create(cr, uid, {
576                 'name': order.name,
577                 'date': order.date_order[:10],
578                 'ref': order.name,
579                 'move_id': move_id,
580                 'company_id': comp_id,
581                 'account_id': order_account,
582                 'credit': ((order.amount_total < 0) and -order.amount_total)\
583                     or 0.0,
584                 'debit': ((order.amount_total > 0) and order.amount_total)\
585                     or 0.0,
586                 'journal_id': order.sale_journal.id,
587                 'period_id': period,
588                 'partner_id': order.partner_id and order.partner_id.id or False
589             }, context=context))
590             self.write(cr, uid, order.id, {'state':'done', 'account_move': move_id}, context=context)
591         return True
592
593     def action_payment(self, cr, uid, ids, context=None):
594         return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
595
596     def action_paid(self, cr, uid, ids, context=None):
597         context = context or {}
598         self.create_picking(cr, uid, ids, context=None)
599         self.write(cr, uid, ids, {'state': 'paid'}, context=context)
600         return True
601
602     def action_cancel(self, cr, uid, ids, context=None):
603         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
604         return True
605
606     def action_done(self, cr, uid, ids, context=None):
607         self.create_account_move(cr, uid, ids, context=context)
608         return True
609
610 pos_order()
611
612 class account_bank_statement(osv.osv):
613     _inherit = 'account.bank.statement'
614     _columns= {
615         'user_id': fields.many2one('res.users', 'User', readonly=True),
616     }
617     _defaults = {
618         'user_id': lambda self,cr,uid,c={}: uid
619     }
620 account_bank_statement()
621
622 class account_bank_statement_line(osv.osv):
623     _inherit = 'account.bank.statement.line'
624     _columns= {
625         'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
626     }
627 account_bank_statement_line()
628
629 class pos_order_line(osv.osv):
630     _name = "pos.order.line"
631     _description = "Lines of Point of Sale"
632     _rec_name = "product_id"
633
634     def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
635         res = dict([(i, {}) for i in ids])
636         account_tax_obj = self.pool.get('account.tax')
637         cur_obj = self.pool.get('res.currency')
638         for line in self.browse(cr, uid, ids, context=context):
639             taxes = line.product_id.taxes_id
640             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
641             taxes = account_tax_obj.compute_all(cr, uid, line.product_id.taxes_id, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
642
643             cur = line.order_id.pricelist_id.currency_id
644             res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
645             res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
646         return res
647
648     def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
649        context = context or {}
650        if not product_id:
651             return {}
652        if not pricelist:
653            raise osv.except_osv(_('No Pricelist !'),
654                _('You have to select a pricelist in the sale form !\n' \
655                'Please set one before choosing a product.'))
656
657        price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
658                product_id, qty or 1.0, partner_id)[pricelist]
659
660        result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
661        result['value']['price_unit'] = price
662        return result
663
664     def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
665         result = {}
666         if not product:
667             return result
668         account_tax_obj = self.pool.get('account.tax')
669         cur_obj = self.pool.get('res.currency')
670
671         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
672
673         taxes = prod.taxes_id
674         price = price_unit * (1 - (discount or 0.0) / 100.0)
675         taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
676
677         result['price_subtotal'] = taxes['total']
678         result['price_subtotal_incl'] = taxes['total_included']
679         return {'value': result}
680
681     _columns = {
682         'company_id': fields.many2one('res.company', 'Company', required=True),
683         'name': fields.char('Line No', size=32, required=True),
684         'notice': fields.char('Discount Notice', size=128),
685         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
686         'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
687         'qty': fields.float('Quantity', digits=(16, 2)),
688         'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
689         'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
690         'discount': fields.float('Discount (%)', digits=(16, 2)),
691         'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
692         'create_date': fields.datetime('Creation Date', readonly=True),
693     }
694
695     _defaults = {
696         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
697         'qty': lambda *a: 1,
698         'discount': lambda *a: 0.0,
699         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
700     }
701
702     def copy_data(self, cr, uid, id, default=None, context=None):
703         if not default:
704             default = {}
705         default.update({
706             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
707         })
708         return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
709
710 pos_order_line()
711
712 class pos_category(osv.osv):
713     _name = 'pos.category'
714     _description = "PoS Category"
715     _order = "sequence, name"
716     def _check_recursion(self, cr, uid, ids, context=None):
717         level = 100
718         while len(ids):
719             cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
720             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
721             if not level:
722                 return False
723             level -= 1
724         return True
725
726     _constraints = [
727         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
728     ]
729
730     def name_get(self, cr, uid, ids, context=None):
731         if not len(ids):
732             return []
733         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
734         res = []
735         for record in reads:
736             name = record['name']
737             if record['parent_id']:
738                 name = record['parent_id'][1]+' / '+name
739             res.append((record['id'], name))
740         return res
741
742     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
743         res = self.name_get(cr, uid, ids, context=context)
744         return dict(res)
745
746     _columns = {
747         'name': fields.char('Name', size=64, required=True, translate=True),
748         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
749         'parent_id': fields.many2one('pos.category','Parent Category', select=True),
750         'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
751         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
752     }
753 pos_category()
754
755 import io, StringIO
756
757 class product_product(osv.osv):
758     _inherit = 'product.product'
759     def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
760         result = {}
761         for obj in self.browse(cr, uid, ids, context=context):
762             if not obj.product_image:
763                 result[obj.id] = False
764                 continue
765
766             image_stream = io.BytesIO(obj.product_image.decode('base64'))
767             img = Image.open(image_stream)
768             img.thumbnail((120, 100), Image.ANTIALIAS)
769             img_stream = StringIO.StringIO()
770             img.save(img_stream, "JPEG")
771             result[obj.id] = img_stream.getvalue().encode('base64')
772         return result
773
774     _columns = {
775         'income_pdt': fields.boolean('PoS Cash Input', help="This is a product you can use to put cash into a statement for the point of sale backend."),
776         'expense_pdt': fields.boolean('PoS Cash Output', help="This is a product you can use to take cash from a statement for the point of sale backend, exemple: money lost, transfer to bank, etc."),
777         'pos_categ_id': fields.many2one('pos.category','PoS Category',
778             help="If you want to sell this product through the point of sale, select the category it belongs to."),
779         'product_image_small': fields.function(_get_small_image, string='Small Image', type="binary",
780             store = {
781                 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['product_image'], 10),
782             })
783     }
784 product_product()
785
786
787 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: