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