c59aa86f5c90a635b7a09ba0403d749b35733614
[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(osv.osv):
37     _name = 'pos.config'
38
39     POS_CONFIG_STATE = [
40         ('draft', 'Draft'),
41         ('active', 'Active'),
42         ('inactive', 'Inactive'),
43         ('deprecated', 'Deprecated')
44     ]
45
46     _columns = {
47         'name' : fields.char('Name', size=32,
48                              select=1,
49                              required=True,
50 #                             readonly=True,
51 #                             states={'draft' : [('readonly', False)]}
52                             ),
53         'journal_ids' : fields.many2many('account.journal', 
54                                          'pos_config_journal_rel', 
55                                          'pos_config_id', 
56                                          'journal_id', 
57                                          'Payment Methods',
58                                          domain="[('journal_user', '=', True )]",
59 #                                         readonly=True,
60 #                                         states={'draft' : [('readonly', False)]}
61                                         ),
62         'shop_id' : fields.many2one('sale.shop', 'Shop',
63                                     required=True,
64                                     select=1,
65 #                                    readonly=True,
66 #                                    states={'draft' : [('readonly', False)]} 
67                                    ),
68         'journal_id' : fields.many2one('account.journal', 'Journal',
69                                        required=True,
70                                        select=1,
71                                        domain=[('type', '=', 'sale')],
72 #                                       readonly=True,
73 #                                       states={'draft' : [('readonly', False)]}
74                                       ),
75         'iface_self_checkout' : fields.boolean('Self Checkout Mode'),
76         'iface_websql' : fields.boolean('WebSQL (to store data)'),
77         'iface_led' : fields.boolean('LED Interface'),
78         'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'),
79         'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'),
80         'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'),
81         'iface_barscan' : fields.boolean('BarScan Interface'), 
82         'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'),
83         'iface_print_via_proxy' : fields.boolean('Print via Proxy'),
84
85         'state' : fields.selection(POS_CONFIG_STATE, 'State',
86                                    required=True,
87                                    readonly=True),
88
89         'sequence_id' : fields.many2one('ir.sequence', 'Sequence',
90                                         readonly=True),
91         'user_id' : fields.many2one('res.users', 'User',
92 #                                    readonly=True,
93 #                                    states={'draft' : [('readonly', False)]}
94                                    ),
95
96     }
97
98     _defaults = {
99         'state' : 'draft',
100         'user_id' : lambda obj, cr, uid, context: uid,
101     }
102
103     def _check_only_one_cash_journal(self, cr, uid, ids, context=None):
104         for record in self.browse(cr, uid, ids, context=context):
105             has_cash_journal = False
106
107             for journal in record.journal_ids:
108                 if journal.type == 'cash':
109                     if has_cash_journal:
110                         return False
111                     else:
112                         has_cash_journal = True
113         return True
114
115     _constraints = [
116         (_check_only_one_cash_journal, "You should have only one Cash Journal !", ['journal_id']),
117     ]
118
119     def set_draft(self, cr, uid, ids, context=None):
120         return self.write(cr, uid, ids, {'state' : 'draft'}, context=context)
121
122     def set_active(self, cr, uid, ids, context=None):
123         return self.write(cr, uid, ids, {'state' : 'active'}, context=context)
124
125     def set_inactive(self, cr, uid, ids, context=None):
126         return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context)
127
128     def set_deprecate(self, cr, uid, ids, context=None):
129         return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context)
130
131     def create(self, cr, uid, values, context=None):
132         proxy = self.pool.get('ir.sequence.type')
133
134         sequence_values = dict(
135             code='pos_%s_sequence' % values['name'].lower(),
136             name='POS %s Sequence' % values['name'],
137         )
138
139         proxy.create(cr, uid, sequence_values, context=context)
140
141         proxy = self.pool.get('ir.sequence')
142
143         sequence_values = dict(
144             code='pos_%s_sequence' % values['name'].lower(),
145             name='POS %s Sequence' % values['name'],
146             padding=4,
147             prefix="%s/%%(year)s/%%(month)s/%%(day)s/"  % values['name'],
148         )
149         sequence_id = proxy.create(cr, uid, sequence_values, context=context)
150
151         values['sequence_id'] = sequence_id
152         return super(pos_config, self).create(cr, uid, values, context=context)
153
154     def write(self, cr, uid, ids, values, context=None):
155         for obj in self.browse(cr, uid, ids, context=context):
156             if obj.sequence_id and values.get('name', False):
157                 prefixes = obj.sequence_id.prefix.split('/')
158                 if len(prefixes) >= 4 and prefixes[0] == obj.name:
159                     prefixes[0] = values['name']
160
161                 sequence_values = dict(
162                     code='pos_%s_sequence' % values['name'].lower(),
163                     name='POS %s Sequence' % values['name'],
164                     prefix="/".join(prefixes),
165                 )
166                 obj.sequence_id.write(sequence_values)
167
168         return super(pos_config, self).write(cr, uid, ids, values, context=context)
169
170     def unlink(self, cr, uid, ids, context=None):
171         for obj in self.browse(cr, uid, ids, context=context):
172             if obj.sequence_id:
173                 obj.sequence_id.unlink()
174
175         return super(pos_config, self).unlink(cr, uid, ids, context=context)
176
177 pos_config()
178
179 class pos_session(osv.osv):
180     _name = 'pos.session'
181
182     POS_SESSION_STATE = [
183         ('opening_control', 'Opening Control'),  # Signal open
184         ('opened', 'Opened'),                    # Signal closing
185         ('closing_control', 'Closing Control'),  # Signal close
186         ('closed', 'Closed'),
187     ]
188
189     def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None):
190         result = dict.fromkeys(ids, False)
191         for record in self.browse(cr, uid, ids, context=context):
192             cash_register_id = False
193             for bank_statement in record.statement_ids:
194                 if bank_statement.journal_id.type == 'cash':
195                     cash_register_id = bank_statement.id
196                     break
197             result[record.id] = cash_register_id
198
199         return result
200
201     _columns = {
202         'config_id' : fields.many2one('pos.config', 'PoS',
203                                       required=True,
204                                       select=1,
205                                       domain="[('state', '=', 'active')]",
206 #                                      readonly=True,
207 #                                      states={'draft' : [('readonly', False)]}
208                                      ),
209
210         'name' : fields.char('Session Sequence', size=32,
211                              required=True,
212                              select=1,
213 #                             readonly=True,
214 #                             states={'draft' : [('readonly', False)]}
215                             ),
216         'user_id' : fields.many2one('res.users', 'User',
217                                     required=True,
218                                     select=1,
219 #                                    readonly=True,
220 #                                    states={'draft' : [('readonly', False)]}
221                                    ),
222         'start_at' : fields.datetime('Opening Date'), 
223         'stop_at' : fields.datetime('Closing Date'),
224
225         'state' : fields.selection(POS_SESSION_STATE, 'State',
226                                    required=True,
227                                    readonly=True,
228                                    select=1),
229
230         'cash_register_id' : fields.function(_compute_cash_register_id, method=True, 
231                                              type='many2one', relation='account.bank.statement',
232                                              string='Cash Register', store=True),
233
234         'details_ids' : fields.related('cash_register_id', 'details_ids', 
235                                        type='one2many', relation='account.cashbox.line',
236                                        string='CashBox Lines'),
237         'journal_ids' : fields.related('config_id', 'journal_ids',
238                                        type='many2many',
239                                        readonly=True,
240                                        relation='account.journal',
241                                        string='Journals'),
242         'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'),
243
244         'statement_ids' : fields.many2many('account.bank.statement', 
245                                            'pos_session_statement_rel',
246                                            'session_id',
247                                            'statement_id',
248                                            'Bank Statement',
249                                            readonly=True),
250     }
251
252     _defaults = {
253         'name' : '/',
254         'user_id' : lambda obj, cr, uid, context: uid,
255         'state' : 'opening_control',
256     }
257
258     _sql_constraints = [
259         ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"),
260     ]
261
262     def create(self, cr, uid, values, context=None):
263         config_id = values.get('config_id', False) or False
264
265         pos_config = None
266         if config_id:
267             pos_config = self.pool.get('pos.config').browse(cr, uid, config_id, context=context)
268
269             bank_statement_ids = []
270             for journal in pos_config.journal_ids:
271                 bank_values = {
272                     'journal_id' : journal.id,
273                     'user_id' : pos_config.user_id and pos_config.user_id.id or uid,
274                 }
275
276                 statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context)
277
278                 bank_statement_ids.append(statement_id) 
279
280             values.update({
281                 'name' : pos_config.sequence_id._next(),
282                 'statement_ids' : [(6, 0, bank_statement_ids)]
283             })  
284
285         return super(pos_session, self).create(cr, uid, values, context=context)
286
287     def unlink(self, cr, uid, ids, context=None):
288         for obj in self.browse(cr, uid, ids, context=context):
289             for statement in obj.statement_ids:
290                 statement.unlink(context=context)
291         return True
292
293     def on_change_config(self, cr, uid, ids, config_id, context=None):
294         result = dict(value=dict())
295         if not config_id:
296             result['value']['user_id'] = uid
297         else:
298             result['value']['user_id'] = self.pool.get('pos.config').browse(cr, uid, config_id, context=context).user_id.id
299
300         return result            
301
302     def wkf_action_open(self, cr, uid, ids, context=None):
303         # si pas de date start_at, je balance une date, sinon on utilise celle de l'utilisateur
304         for record in self.browse(cr, uid, ids, context=context):
305             values = {}
306             if not record.start_at:
307                 values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
308             values['state'] = 'opened'
309
310             record.write(values, context=context)
311
312             for st in record.statement_ids:
313                 st.button_open(context=context)
314
315         return True
316
317     def wkf_action_closing_control(self, cr, uid, ids, context=None):
318         # Close CashBox
319         for record in self.browse(cr, uid, ids, context=context):
320             for st in record.statement_ids:
321                 getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context)
322
323         return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
324
325     def wkf_action_close(self, cr, uid, ids, context=None):
326         self._confirm_orders(cr, uid, ids, context=context)
327         return self.write(cr, uid, ids, {'state' : 'closed'}, context=context)
328
329     def _confirm_orders(self, cr, uid, ids, context=None):
330         wf_service = netsvc.LocalService("workflow")
331
332         for session in self.browse(cr, uid, ids, context=context):
333             for order in session.order_ids:
334                 if order.state != 'paid':
335                     raise osv.except_osv(
336                         _('Error !'),
337                         _("You can not confirm all orders of this session, because they have not the 'paid' status"))
338                 else:
339                     wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr)
340
341         return True
342
343     def get_current_session(self, cr, uid, context=None):
344         current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
345         domain = [
346             ('state', '=', 'open'),
347             ('start_at', '>=', time.strftime('%Y-%m-%d 00:00:00')),
348             ('user_id', '=', uid),
349         ]
350         session_ids = self.search(cr, uid, domain, context=context, limit=1, order='start_at desc')
351         session_id = session_ids[0] if session_ids else False
352
353         if not session_id:
354             pos_config_proxy = self.pool.get('pos.config')
355             domain = [
356                 ('user_id', '=', uid),
357                 ('state', '=', 'active'),
358             ]
359             pos_config_ids = pos_config_proxy.search(cr, uid, domain,
360                                                      limit=1,
361                                                      order='create_date desc',
362                                                      context=context)
363
364             if not pos_config_ids:
365                 raise osv.except_osv(_('Error !'),
366                                      _('There is no active PoS Config for this User %s') % current_user.name)
367
368             config = pos_config_proxy.browse(cr, uid, pos_config_ids[0], context=context)
369
370             values = {
371                 'state' : 'new',
372                 'start_at' : time.strftime('%Y-%m-%d %H:%M:%S'),
373                 'config_id' : config.id,
374                 'journal_id' : config.journal_id.id,
375                 'user_id': current_user.id,
376             }
377
378             session_id = self.create(cr, uid, values, context=context)
379             wkf_service = netsvc.LocalService('workflow')
380             wkf_service.trg_validate(uid, 'pos.session', session_id, 'opening_control', cr)
381             
382         return session_id
383
384 pos_session()
385
386 class pos_config_journal(osv.osv):
387     """ Point of Sale journal configuration"""
388     _name = 'pos.config.journal'
389     _description = "Journal Configuration"
390
391     _columns = {
392         'name': fields.char('Description', size=64),
393         'code': fields.char('Code', size=64),
394         'journal_id': fields.many2one('account.journal', "Journal")
395     }
396
397 pos_config_journal()
398
399 class pos_order(osv.osv):
400     _name = "pos.order"
401     _description = "Point of Sale"
402     _order = "id desc"
403
404     def create_from_ui(self, cr, uid, orders, context=None):
405         #_logger.info("orders: %r", orders)
406         list = []
407         session_id = self.pool.get('pos.session').get_current_session(cr, uid, context=context)
408         for order in orders:
409             # 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}
410             order['session_id'] = session_id
411             order_obj = self.pool.get('pos.order')
412             # get statements out of order because they will be generated with add_payment to ensure
413             # the module behavior is the same when using the front-end or the back-end
414             if not order['data']['statement_ids']:
415                 continue
416             statement_ids = order['data'].pop('statement_ids')
417             order_id = self.create(cr, uid, order, context)
418             list.append(order_id)
419             # call add_payment; refer to wizard/pos_payment for data structure
420             # add_payment launches the 'paid' signal to advance the workflow to the 'paid' state
421             data = {
422                 'journal': statement_ids[0][2]['journal_id'],
423                 'amount': order['data']['amount_paid'],
424                 'payment_name': order['data']['name'],
425                 'payment_date': statement_ids[0][2]['name'],
426             }
427             order_obj.add_payment(cr, uid, order_id, data, context=context)
428         return list
429
430     def unlink(self, cr, uid, ids, context=None):
431         for rec in self.browse(cr, uid, ids, context=context):
432             if rec.state not in ('draft','cancel'):
433                 raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.'))
434         return super(pos_order, self).unlink(cr, uid, ids, context=context)
435
436     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
437         if not part:
438             return {'value': {}}
439         pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
440         return {'value': {'pricelist_id': pricelist}}
441
442     def _amount_all(self, cr, uid, ids, name, args, context=None):
443         tax_obj = self.pool.get('account.tax')
444         cur_obj = self.pool.get('res.currency')
445         res = {}
446         for order in self.browse(cr, uid, ids, context=context):
447             res[order.id] = {
448                 'amount_paid': 0.0,
449                 'amount_return':0.0,
450                 'amount_tax':0.0,
451             }
452             val1 = val2 = 0.0
453             cur = order.pricelist_id.currency_id
454             for payment in order.statement_ids:
455                 res[order.id]['amount_paid'] +=  payment.amount
456                 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
457             for line in order.lines:
458                 val1 += line.price_subtotal_incl
459                 val2 += line.price_subtotal
460             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
461             res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
462         return res
463
464     def _default_sale_journal(self, cr, uid, context=None):
465         res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1)
466         return res and res[0] or False
467
468     def _default_shop(self, cr, uid, context=None):
469         res = self.pool.get('sale.shop').search(cr, uid, [])
470         return res and res[0] or False
471
472     def copy(self, cr, uid, id, default=None, context=None):
473         if not default:
474             default = {}
475         d = {
476             'state': 'draft',
477             'invoice_id': False,
478             'account_move': False,
479             'picking_id': False,
480             'statement_ids': [],
481             'nb_print': 0,
482             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
483         }
484         d.update(default)
485         return super(pos_order, self).copy(cr, uid, id, d, context=context)
486
487     _columns = {
488         'name': fields.char('Order Ref', size=64, required=True, readonly=True),
489         'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
490         'shop_id': fields.many2one('sale.shop', 'Shop', required=True,
491             states={'draft': [('readonly', False)]}, readonly=True),
492         'date_order': fields.datetime('Date Ordered', readonly=True, select=True),
493         '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."),
494         'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
495         'amount_total': fields.function(_amount_all, string='Total', multi='all'),
496         'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
497         'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
498         'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
499         'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
500         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
501         'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
502
503         'session_id' : fields.many2one('pos.session', 'Session', 
504                                         #required=True,
505                                         select=1,
506                                         domain="[('state', '=', 'opened')]",
507                                         states={'draft' : [('readonly', False)]},
508                                         readonly=True),
509
510         'state': fields.selection([('draft', 'New'),
511                                    ('cancel', 'Cancelled'),
512                                    ('paid', 'Paid'),
513                                    ('done', 'Posted'),
514                                    ('invoiced', 'Invoiced')],
515                                   'State', readonly=True),
516
517         'invoice_id': fields.many2one('account.invoice', 'Invoice'),
518         'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
519         'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
520         'note': fields.text('Internal Notes'),
521         'nb_print': fields.integer('Number of Print', readonly=True),
522         'sale_journal': fields.many2one('account.journal', 'Journal', required=True, states={'draft': [('readonly', False)]}, readonly=True),
523     }
524
525     def _default_pricelist(self, cr, uid, context=None):
526         res = self.pool.get('sale.shop').search(cr, uid, [], context=context)
527         if res:
528             shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context)
529             return shop.pricelist_id and shop.pricelist_id.id or False
530         return False
531
532     _defaults = {
533         'user_id': lambda self, cr, uid, context: uid,
534         'state': 'draft',
535         'name': '/', 
536         'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
537         'nb_print': 0,
538         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
539         'sale_journal': _default_sale_journal,
540         'shop_id': _default_shop,
541         'pricelist_id': _default_pricelist,
542     }
543
544     def create(self, cr, uid, values, context=None):
545         values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order')
546         return super(pos_order, self).create(cr, uid, values, context=context)
547
548     def test_paid(self, cr, uid, ids, context=None):
549         """A Point of Sale is paid when the sum
550         @return: True
551         """
552         for order in self.browse(cr, uid, ids, context=context):
553             if order.lines and not order.amount_total:
554                 return True
555             if (not order.lines) or (not order.statement_ids) or \
556                 (abs(order.amount_total-order.amount_paid) > 0.00001):
557                 return False
558         return True
559
560     def create_picking(self, cr, uid, ids, context=None):
561         """Create a picking for each order and validate it."""
562         picking_obj = self.pool.get('stock.picking')
563         partner_obj = self.pool.get('res.partner')
564         move_obj = self.pool.get('stock.move')
565
566         for order in self.browse(cr, uid, ids, context=context):
567             if not order.state=='draft':
568                 continue
569             addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
570             picking_id = picking_obj.create(cr, uid, {
571                 'origin': order.name,
572                 'partner_id': addr.get('delivery',False),
573                 'type': 'out',
574                 'company_id': order.company_id.id,
575                 'move_type': 'direct',
576                 'note': order.note or "",
577                 'invoice_state': 'none',
578                 'auto_picking': True,
579             }, context=context)
580             self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
581             location_id = order.shop_id.warehouse_id.lot_stock_id.id
582             output_id = order.shop_id.warehouse_id.lot_output_id.id
583
584             for line in order.lines:
585                 if line.product_id and line.product_id.type == 'service':
586                     continue
587                 if line.qty < 0:
588                     location_id, output_id = output_id, location_id
589
590                 move_obj.create(cr, uid, {
591                     'name': line.name,
592                     'product_uom': line.product_id.uom_id.id,
593                     'product_uos': line.product_id.uom_id.id,
594                     'picking_id': picking_id,
595                     'product_id': line.product_id.id,
596                     'product_uos_qty': abs(line.qty),
597                     'product_qty': abs(line.qty),
598                     'tracking_id': False,
599                     'state': 'draft',
600                     'location_id': location_id,
601                     'location_dest_id': output_id,
602                 }, context=context)
603                 if line.qty < 0:
604                     location_id, output_id = output_id, location_id
605
606             wf_service = netsvc.LocalService("workflow")
607             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
608             picking_obj.force_assign(cr, uid, [picking_id], context)
609         return True
610
611     def set_to_draft(self, cr, uid, ids, *args):
612         if not len(ids):
613             return False
614         for order in self.browse(cr, uid, ids, context=context):
615             if order.state != 'cancel':
616                 raise osv.except_osv(_('Error!'), _('In order to set to draft a sale, it must be cancelled.'))
617         self.write(cr, uid, ids, {'state': 'draft'})
618         wf_service = netsvc.LocalService("workflow")
619         for i in ids:
620             wf_service.trg_create(uid, 'pos.order', i, cr)
621         return True
622
623     def cancel_order(self, cr, uid, ids, context=None):
624         """ Changes order state to cancel
625         @return: True
626         """
627         stock_picking_obj = self.pool.get('stock.picking')
628         for order in self.browse(cr, uid, ids, context=context):
629             wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr)
630             if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
631                 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
632         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
633         return True
634
635     def add_payment(self, cr, uid, order_id, data, context=None):
636         """Create a new payment for the order"""
637         if not context:
638             context = {}
639         statement_obj = self.pool.get('account.bank.statement')
640         statement_line_obj = self.pool.get('account.bank.statement.line')
641         prod_obj = self.pool.get('product.product')
642         property_obj = self.pool.get('ir.property')
643         curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
644         curr_company = curr_c.id
645         order = self.browse(cr, uid, order_id, context=context)
646         ids_new = []
647         args = {
648             'amount': data['amount'],
649         }
650         if 'payment_date' in data.keys():
651             args['date'] = data['payment_date']
652         args['name'] = order.name
653         if data.get('payment_name', False):
654             args['name'] = args['name'] + ': ' + data['payment_name']
655         account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
656         args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
657                              and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
658         args['partner_id'] = order.partner_id and order.partner_id.id or None
659
660         if not args['account_id']:
661             if not args['partner_id']:
662                 msg = _('There is no receivable account defined to make payment')
663             else:
664                 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d)') % (order.partner_id.name, order.partner_id.id,)
665             raise osv.except_osv(_('Configuration Error !'), msg)
666
667         context.pop('pos_session_id', False)
668
669         try:
670             journal_id = long(data['journal'])
671         except Exception:
672             journal_id = False
673
674         statement_id = False
675         for statement in order.session_id.statement_ids:
676             if statement.journal_id.id == journal_id:
677                 statement_id = statement.id
678                 break
679
680         if not statement_id:
681             raise osv.except_osv(_('Error !'), _('You have to open at least one cashbox'))
682
683         args.update({
684             'statement_id' : statement_id,
685             'pos_statement_id' : order_id,
686             'journal_id' : journal_id,
687             'type' : 'customer',
688             'ref' : order.name,
689         })
690
691         statement_line_obj.create(cr, uid, args, context=context)
692         ids_new.append(statement_id)
693
694         wf_service = netsvc.LocalService("workflow")
695         wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
696         wf_service.trg_write(uid, 'pos.order', order_id, cr)
697
698         return statement_id
699
700     def refund(self, cr, uid, ids, context=None):
701         """Create a copy of order  for refund order"""
702         clone_list = []
703         line_obj = self.pool.get('pos.order.line')
704         for order in self.browse(cr, uid, ids, context=context):
705             clone_id = self.copy(cr, uid, order.id, {
706                 'name': order.name + ' REFUND',
707             }, context=context)
708             clone_list.append(clone_id)
709
710         for clone in self.browse(cr, uid, clone_list, context=context):
711             for order_line in clone.lines:
712                 line_obj.write(cr, uid, [order_line.id], {
713                     'qty': -order_line.qty
714                 }, context=context)
715
716         new_order = ','.join(map(str,clone_list))
717         abs = {
718             #'domain': "[('id', 'in', ["+new_order+"])]",
719             'name': _('Return Products'),
720             'view_type': 'form',
721             'view_mode': 'form',
722             'res_model': 'pos.order',
723             'res_id':clone_list[0],
724             'view_id': False,
725             'context':context,
726             'type': 'ir.actions.act_window',
727             'nodestroy': True,
728             'target': 'current',
729         }
730         return abs
731
732     def action_invoice_state(self, cr, uid, ids, context=None):
733         return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
734
735     def action_invoice(self, cr, uid, ids, context=None):
736         wf_service = netsvc.LocalService("workflow")
737         inv_ref = self.pool.get('account.invoice')
738         inv_line_ref = self.pool.get('account.invoice.line')
739         product_obj = self.pool.get('product.product')
740         inv_ids = []
741
742         for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
743             if order.invoice_id:
744                 inv_ids.append(order.invoice_id.id)
745                 continue
746
747             if not order.partner_id:
748                 raise osv.except_osv(_('Error'), _('Please provide a partner for the sale.'))
749
750             acc = order.partner_id.property_account_receivable.id
751             inv = {
752                 'name': order.name,
753                 'origin': order.name,
754                 'account_id': acc,
755                 'journal_id': order.sale_journal.id or None,
756                 'type': 'out_invoice',
757                 'reference': order.name,
758                 'partner_id': order.partner_id.id,
759                 'comment': order.note or '',
760                 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
761             }
762             inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
763             if not inv.get('account_id', None):
764                 inv['account_id'] = acc
765             inv_id = inv_ref.create(cr, uid, inv, context=context)
766
767             self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
768             inv_ids.append(inv_id)
769             for line in order.lines:
770                 inv_line = {
771                     'invoice_id': inv_id,
772                     'product_id': line.product_id.id,
773                     'quantity': line.qty,
774                 }
775                 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
776                 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
777                                                                line.product_id.id,
778                                                                line.product_id.uom_id.id,
779                                                                line.qty, partner_id = order.partner_id.id,
780                                                                fposition_id=order.partner_id.property_account_position.id)['value'])
781                 if line.product_id.description_sale:
782                     inv_line['note'] = line.product_id.description_sale
783                 inv_line['price_unit'] = line.price_unit
784                 inv_line['discount'] = line.discount
785                 inv_line['name'] = inv_name
786                 inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
787                     and [(6, 0, inv_line['invoice_line_tax_id'])] or []
788                 inv_line_ref.create(cr, uid, inv_line, context=context)
789             inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
790             wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
791
792         if not inv_ids: return {}
793
794         mod_obj = self.pool.get('ir.model.data')
795         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
796         res_id = res and res[1] or False
797         return {
798             'name': _('Customer Invoice'),
799             'view_type': 'form',
800             'view_mode': 'form',
801             'view_id': [res_id],
802             'res_model': 'account.invoice',
803             'context': "{'type':'out_invoice'}",
804             'type': 'ir.actions.act_window',
805             'nodestroy': True,
806             'target': 'current',
807             'res_id': inv_ids and inv_ids[0] or False,
808         }
809
810     def create_account_move(self, cr, uid, ids, context=None):
811         """Create a account move line of order grouped by products or not."""
812         account_move_obj = self.pool.get('account.move')
813         account_move_line_obj = self.pool.get('account.move.line')
814         account_period_obj = self.pool.get('account.period')
815         period = account_period_obj.find(cr, uid, context=context)[0]
816         account_tax_obj = self.pool.get('account.tax')
817         res_obj=self.pool.get('res.users')
818         property_obj=self.pool.get('ir.property')
819
820         for order in self.browse(cr, uid, ids, context=context):
821             if order.state != 'paid':
822                 continue
823
824             curr_c = res_obj.browse(cr, uid, uid).company_id
825             comp_id = res_obj.browse(cr, order.user_id.id, order.user_id.id).company_id
826             comp_id = comp_id and comp_id.id or False
827             to_reconcile = []
828             group_tax = {}
829             account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
830
831             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
832
833             # Create an entry for the sale
834             move_id = account_move_obj.create(cr, uid, {
835                 'ref' : order.name,
836                 'journal_id': order.sale_journal.id,
837             }, context=context)
838
839             # Create an move for each order line
840             for line in order.lines:
841                 tax_amount = 0
842                 taxes = [t for t in line.product_id.taxes_id]
843                 computed = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)
844                 computed_taxes = computed['taxes']
845
846                 for tax in computed_taxes:
847                     tax_amount += round(tax['amount'], 2)
848                     group_key = (tax['tax_code_id'],
849                                 tax['base_code_id'],
850                                 tax['account_collected_id'])
851
852                     if group_key in group_tax:
853                         group_tax[group_key] += round(tax['amount'], 2)
854                     else:
855                         group_tax[group_key] = round(tax['amount'], 2)
856                 amount = line.price_subtotal
857
858                 # Search for the income account
859                 if  line.product_id.property_account_income.id:
860                     income_account = line.product_id.property_account_income.id
861                 elif line.product_id.categ_id.property_account_income_categ.id:
862                     income_account = line.product_id.categ_id.property_account_income_categ.id
863                 else:
864                     raise osv.except_osv(_('Error !'), _('There is no income '\
865                         'account defined for this product: "%s" (id:%d)') \
866                         % (line.product_id.name, line.product_id.id, ))
867
868                 # Empty the tax list as long as there is no tax code:
869                 tax_code_id = False
870                 tax_amount = 0
871                 while computed_taxes:
872                     tax = computed_taxes.pop(0)
873                     if amount > 0:
874                         tax_code_id = tax['base_code_id']
875                         tax_amount = line.price_subtotal * tax['base_sign']
876                     else:
877                         tax_code_id = tax['ref_base_code_id']
878                         tax_amount = line.price_subtotal * tax['ref_base_sign']
879                     # If there is one we stop
880                     if tax_code_id:
881                         break
882
883                 # Create a move for the line
884                 account_move_line_obj.create(cr, uid, {
885                     'name': line.product_id.name,
886                     'date': order.date_order[:10],
887                     'ref': order.name,
888                     'quantity': line.qty,
889                     'product_id': line.product_id.id,
890                     'move_id': move_id,
891                     'account_id': income_account,
892                     'company_id': comp_id,
893                     'credit': ((amount>0) and amount) or 0.0,
894                     'debit': ((amount<0) and -amount) or 0.0,
895                     'journal_id': order.sale_journal.id,
896                     'period_id': period,
897                     'tax_code_id': tax_code_id,
898                     'tax_amount': tax_amount,
899                     'partner_id': order.partner_id and order.partner_id.id or False
900                 }, context=context)
901
902                 # For each remaining tax with a code, whe create a move line
903                 for tax in computed_taxes:
904                     if amount > 0:
905                         tax_code_id = tax['base_code_id']
906                         tax_amount = line.price_subtotal * tax['base_sign']
907                     else:
908                         tax_code_id = tax['ref_base_code_id']
909                         tax_amount = line.price_subtotal * tax['ref_base_sign']
910                     if not tax_code_id:
911                         continue
912
913                     account_move_line_obj.create(cr, uid, {
914                         'name': "Tax" + line.name +  " (%s)" % (tax.name),
915                         'date': order.date_order[:10],
916                         'ref': order.name,
917                         'product_id':line.product_id.id,
918                         'quantity': line.qty,
919                         'move_id': move_id,
920                         'account_id': income_account,
921                         'company_id': comp_id,
922                         'credit': 0.0,
923                         'debit': 0.0,
924                         'journal_id': order.sale_journal.id,
925                         'period_id': period,
926                         'tax_code_id': tax_code_id,
927                         'tax_amount': tax_amount,
928                     }, context=context)
929
930
931             # Create a move for each tax group
932             (tax_code_pos, base_code_pos, account_pos)= (0, 1, 2)
933             for key, amount in group_tax.items():
934                 account_move_line_obj.create(cr, uid, {
935                     'name': 'Tax',
936                     'date': order.date_order[:10],
937                     'ref': order.name,
938                     'move_id': move_id,
939                     'company_id': comp_id,
940                     'quantity': line.qty,
941                     'product_id': line.product_id.id,
942                     'account_id': key[account_pos],
943                     'credit': ((amount>0) and amount) or 0.0,
944                     'debit': ((amount<0) and -amount) or 0.0,
945                     'journal_id': order.sale_journal.id,
946                     'period_id': period,
947                     'tax_code_id': key[tax_code_pos],
948                     'tax_amount': amount,
949                 }, context=context)
950
951             # counterpart
952             to_reconcile.append(account_move_line_obj.create(cr, uid, {
953                 'name': "Trade Receivables", #order.name,
954                 'date': order.date_order[:10],
955                 'ref': order.name,
956                 'move_id': move_id,
957                 'company_id': comp_id,
958                 'account_id': order_account,
959                 'credit': ((order.amount_total < 0) and -order.amount_total)\
960                     or 0.0,
961                 'debit': ((order.amount_total > 0) and order.amount_total)\
962                     or 0.0,
963                 'journal_id': order.sale_journal.id,
964                 'period_id': period,
965                 'partner_id': order.partner_id and order.partner_id.id or False
966             }, context=context))
967
968             self.write(cr, uid, order.id, {'state':'done', 'account_move': move_id}, context=context)
969         return True
970
971     def action_payment(self, cr, uid, ids, context=None):
972         return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
973
974     def action_paid(self, cr, uid, ids, context=None):
975         self.create_picking(cr, uid, ids, context=context)
976         self.write(cr, uid, ids, {'state': 'paid'}, context=context)
977         return True
978
979     def action_cancel(self, cr, uid, ids, context=None):
980         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
981         return True
982
983     def action_done(self, cr, uid, ids, context=None):
984         self.create_account_move(cr, uid, ids, context=context)
985         return True
986
987 pos_order()
988
989 class account_bank_statement(osv.osv):
990     _inherit = 'account.bank.statement'
991     _columns= {
992         'user_id': fields.many2one('res.users', 'User', readonly=True),
993     }
994     _defaults = {
995         'user_id': lambda self,cr,uid,c={}: uid
996     }
997 account_bank_statement()
998
999 class account_bank_statement_line(osv.osv):
1000     _inherit = 'account.bank.statement.line'
1001     _columns= {
1002         'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1003     }
1004 account_bank_statement_line()
1005
1006 class pos_order_line(osv.osv):
1007     _name = "pos.order.line"
1008     _description = "Lines of Point of Sale"
1009     _rec_name = "product_id"
1010
1011     def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1012         res = dict([(i, {}) for i in ids])
1013         account_tax_obj = self.pool.get('account.tax')
1014         cur_obj = self.pool.get('res.currency')
1015         for line in self.browse(cr, uid, ids, context=context):
1016             taxes = line.product_id.taxes_id
1017             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1018             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)
1019
1020             cur = line.order_id.pricelist_id.currency_id
1021             res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1022             res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1023         return res
1024
1025     def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1026        context = context or {}
1027        if not product_id:
1028             return {}
1029        if not pricelist:
1030            raise osv.except_osv(_('No Pricelist !'),
1031                _('You have to select a pricelist in the sale form !\n' \
1032                'Please set one before choosing a product.'))
1033
1034        price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1035                product_id, qty or 1.0, partner_id)[pricelist]
1036
1037        result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1038        result['value']['price_unit'] = price
1039        return result
1040
1041     def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1042         result = {}
1043         if not product:
1044             return result
1045         account_tax_obj = self.pool.get('account.tax')
1046         cur_obj = self.pool.get('res.currency')
1047
1048         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1049
1050         taxes = prod.taxes_id
1051         price = price_unit * (1 - (discount or 0.0) / 100.0)
1052         taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1053
1054         result['price_subtotal'] = taxes['total']
1055         result['price_subtotal_incl'] = taxes['total_included']
1056         return {'value': result}
1057
1058     _columns = {
1059         'company_id': fields.many2one('res.company', 'Company', required=True),
1060         'name': fields.char('Line No', size=32, required=True),
1061         'notice': fields.char('Discount Notice', size=128),
1062         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1063         'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1064         'qty': fields.float('Quantity', digits=(16, 2)),
1065         'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1066         'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1067         'discount': fields.float('Discount (%)', digits=(16, 2)),
1068         'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1069         'create_date': fields.datetime('Creation Date', readonly=True),
1070     }
1071
1072     _defaults = {
1073         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1074         'qty': lambda *a: 1,
1075         'discount': lambda *a: 0.0,
1076         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1077     }
1078
1079     def copy_data(self, cr, uid, id, default=None, context=None):
1080         if not default:
1081             default = {}
1082         default.update({
1083             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1084         })
1085         return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1086
1087 pos_order_line()
1088
1089 class pos_category(osv.osv):
1090     _name = 'pos.category'
1091     _description = "PoS Category"
1092     _order = "sequence, name"
1093     def _check_recursion(self, cr, uid, ids, context=None):
1094         level = 100
1095         while len(ids):
1096             cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1097             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1098             if not level:
1099                 return False
1100             level -= 1
1101         return True
1102
1103     _constraints = [
1104         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1105     ]
1106
1107     def name_get(self, cr, uid, ids, context=None):
1108         if not len(ids):
1109             return []
1110         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1111         res = []
1112         for record in reads:
1113             name = record['name']
1114             if record['parent_id']:
1115                 name = record['parent_id'][1]+' / '+name
1116             res.append((record['id'], name))
1117         return res
1118
1119     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1120         res = self.name_get(cr, uid, ids, context=context)
1121         return dict(res)
1122
1123     _columns = {
1124         'name': fields.char('Name', size=64, required=True, translate=True),
1125         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1126         'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1127         'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1128         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1129         'to_weight' : fields.boolean('To Weight'),
1130     }
1131
1132     _defaults = {
1133         'to_weight' : False,
1134     }
1135 pos_category()
1136
1137 import io, StringIO
1138
1139 class product_product(osv.osv):
1140     _inherit = 'product.product'
1141     def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1142         result = {}
1143         for obj in self.browse(cr, uid, ids, context=context):
1144             if not obj.product_image:
1145                 result[obj.id] = False
1146                 continue
1147
1148             image_stream = io.BytesIO(obj.product_image.decode('base64'))
1149             img = Image.open(image_stream)
1150             img.thumbnail((120, 100), Image.ANTIALIAS)
1151             img_stream = StringIO.StringIO()
1152             img.save(img_stream, "JPEG")
1153             result[obj.id] = img_stream.getvalue().encode('base64')
1154         return result
1155
1156     _columns = {
1157         '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."),
1158         '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."),
1159         'pos_categ_id': fields.many2one('pos.category','PoS Category',
1160             help="If you want to sell this product through the point of sale, select the category it belongs to."),
1161         'product_image_small': fields.function(_get_small_image, string='Small Image', type="binary",
1162             store = {
1163                 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['product_image'], 10),
1164             })
1165     }
1166 product_product()
1167
1168
1169 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: