[MERGE] sync with trunk
[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 pdb
23 import openerp
24 import addons
25 import openerp.addons.product.product
26
27 import time
28 from datetime import datetime
29 from dateutil.relativedelta import relativedelta
30 import logging
31
32 import netsvc
33 from osv import fields, osv
34 import tools
35 from tools.translate import _
36 from decimal import Decimal
37 import decimal_precision as dp
38
39 _logger = logging.getLogger(__name__)
40
41 class pos_config(osv.osv):
42     _name = 'pos.config'
43
44     POS_CONFIG_STATE = [
45         ('active', 'Active'),
46         ('inactive', 'Inactive'),
47         ('deprecated', 'Deprecated')
48     ]
49
50     _columns = {
51         'name' : fields.char('Point of Sale Name', size=32, select=1,
52              required=True, help="An internal identification of the point of sale"),
53         'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', 
54              'pos_config_id', 'journal_id', 'Available Payment Methods',
55              domain="[('journal_user', '=', True )]",),
56         'shop_id' : fields.many2one('sale.shop', 'Shop',
57              required=True),
58         'journal_id' : fields.many2one('account.journal', 'Sale Journal',
59              domain=[('type', '=', 'sale')],
60              help="Accounting journal used to post sales entries."),
61         'iface_self_checkout' : fields.boolean('Self Checkout Mode',
62              help="Check this if this point of sale should open by default in a self checkout mode. If unchecked, OpenERP uses the normal cashier mode by default."),
63         'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'),
64         'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'),
65         'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'),
66         'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'),
67         'iface_print_via_proxy' : fields.boolean('Print via Proxy'),
68
69         'state' : fields.selection(POS_CONFIG_STATE, 'State', required=True, readonly=True),
70         'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True,
71             help="This sequence is automatically created by OpenERP but you can change it "\
72                 "to customize the reference numbers of your orders."),
73         'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'),
74         'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"),
75     }
76
77     def _check_cash_control(self, cr, uid, ids, context=None):
78         return all(
79             (sum(int(journal.cash_control) for journal in record.journal_ids) <= 1)
80             for record in self.browse(cr, uid, ids, context=context)
81         )
82
83     _constraints = [
84         (_check_cash_control, "You cannot have two cash controls in one Point Of Sale !", ['journal_ids']),
85     ]
86
87     def copy(self, cr, uid, id, default=None, context=None):
88         if not default:
89             default = {}
90         d = {
91             'sequence_id' : False,
92         }
93         d.update(default)
94         return super(pos_order, self).copy(cr, uid, id, d, context=context)
95
96
97     def name_get(self, cr, uid, ids, context=None):
98         result = []
99         states = {
100             'opening_control': _('Opening Control'),
101             'opened': _('In Progress'),
102             'closing_control': _('Closing Control'),
103             'closed': _('Closed & Posted'),
104         }
105         for record in self.browse(cr, uid, ids, context=context):
106             if (not record.session_ids) or (record.session_ids[0].state=='closed'):
107                 result.append((record.id, record.name+' ('+_('not used')+')'))
108                 continue
109             session = record.session_ids[0]
110             result.append((record.id, record.name + ' ('+session.user_id.name+')')) #, '+states[session.state]+')'))
111         return result
112
113     def _default_sale_journal(self, cr, uid, context=None):
114         res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1)
115         return res and res[0] or False
116
117     def _default_shop(self, cr, uid, context=None):
118         res = self.pool.get('sale.shop').search(cr, uid, [])
119         return res and res[0] or False
120
121     _defaults = {
122         'state' : POS_CONFIG_STATE[0][0],
123         'shop_id': _default_shop,
124         'journal_id': _default_sale_journal,
125         'group_by' : True,
126     }
127
128     def set_active(self, cr, uid, ids, context=None):
129         return self.write(cr, uid, ids, {'state' : 'active'}, context=context)
130
131     def set_inactive(self, cr, uid, ids, context=None):
132         return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context)
133
134     def set_deprecate(self, cr, uid, ids, context=None):
135         return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context)
136
137     def create(self, cr, uid, values, context=None):
138         proxy = self.pool.get('ir.sequence')
139         sequence_values = dict(
140             name='PoS %s' % values['name'],
141             padding=5,
142             prefix="%s/"  % values['name'],
143         )
144         sequence_id = proxy.create(cr, uid, sequence_values, context=context)
145         values['sequence_id'] = sequence_id
146         return super(pos_config, self).create(cr, uid, values, context=context)
147
148     def unlink(self, cr, uid, ids, context=None):
149         for obj in self.browse(cr, uid, ids, context=context):
150             if obj.sequence_id:
151                 obj.sequence_id.unlink()
152         return super(pos_config, self).unlink(cr, uid, ids, context=context)
153
154 class pos_session(osv.osv):
155     _name = 'pos.session'
156     _order = 'id desc'
157
158     POS_SESSION_STATE = [
159         ('opening_control', 'Opening Control'),  # Signal open
160         ('opened', 'In Progress'),                    # Signal closing
161         ('closing_control', 'Closing Control'),  # Signal close
162         ('closed', 'Closed & Posted'),
163     ]
164
165     def _compute_cash_all(self, cr, uid, ids, fieldnames, args, context=None):
166         result = dict()
167
168         for record in self.browse(cr, uid, ids, context=context):
169             result[record.id] = {
170                 'cash_journal_id' : False,
171                 'cash_register_id' : False,
172                 'cash_control' : False,
173             }
174             for st in record.statement_ids:
175                 if st.journal_id.cash_control == True:
176                     result[record.id]['cash_control'] = True
177                     result[record.id]['cash_journal_id'] = st.journal_id.id
178                     result[record.id]['cash_register_id'] = st.id
179
180         return result
181
182     _columns = {
183         'config_id' : fields.many2one('pos.config', 'Point of Sale',
184                                       help="The physical point of sale you will use.",
185                                       required=True,
186                                       select=1,
187                                       domain="[('state', '=', 'active')]",
188                                      ),
189
190         'name' : fields.char('Session ID', size=32, required=True, readonly=True),
191         'user_id' : fields.many2one('res.users', 'Responsible',
192                                     required=True,
193                                     select=1,
194                                     readonly=True,
195                                     states={'opening_control' : [('readonly', False)]}
196                                    ),
197         'start_at' : fields.datetime('Opening Date', readonly=True), 
198         'stop_at' : fields.datetime('Closing Date', readonly=True),
199
200         'state' : fields.selection(POS_SESSION_STATE, 'State',
201                 required=True, readonly=True,
202                 select=1),
203
204         'cash_control' : fields.function(_compute_cash_all,
205                                          multi='cash',
206                                          type='boolean', string='Has Cash Control'),
207         'cash_journal_id' : fields.function(_compute_cash_all,
208                                             multi='cash',
209                                             type='many2one', relation='account.journal',
210                                             string='Cash Journal', store=True),
211         'cash_register_id' : fields.function(_compute_cash_all,
212                                              multi='cash',
213                                              type='many2one', relation='account.bank.statement',
214                                              string='Cash Register', store=True),
215
216         'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids', 
217                 type='one2many', relation='account.cashbox.line',
218                 string='Opening Cash Control'),
219         'details_ids' : fields.related('cash_register_id', 'details_ids', 
220                 type='one2many', relation='account.cashbox.line',
221                 string='Cash Control'),
222
223         'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real',
224                 type='float',
225                 digits_compute=dp.get_precision('Account'),
226                 string="Ending Balance",
227                 help="Computed using the cash control lines",
228                 readonly=True),
229         'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start',
230                 type='float',
231                 digits_compute=dp.get_precision('Account'),
232                 string="Starting Balance",
233                 help="Computed using the cash control at the opening.",
234                 readonly=True),
235         'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding',
236                 string='Total Cash Transaction',
237                 readonly=True),
238         'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end',
239                 type='float',
240                 digits_compute=dp.get_precision('Account'),
241                 string="Computed Balance",
242                 help="Computed with the initial cash control and the sum of all payments.",
243                 readonly=True),
244         'cash_register_difference' : fields.related('cash_register_id', 'difference',
245                 type='float',
246                 string='Difference',
247                 help="Difference between the counted cash control at the closing and the computed balance.",
248                 readonly=True),
249
250         'journal_ids' : fields.related('config_id', 'journal_ids',
251                                        type='many2many',
252                                        readonly=True,
253                                        relation='account.journal',
254                                        string='Available Payment Methods'),
255         'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'),
256
257         'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True),
258     }
259
260     _defaults = {
261         'name' : '/',
262         'user_id' : lambda obj, cr, uid, context: uid,
263         'state' : 'opening_control',
264     }
265
266     _sql_constraints = [
267         ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"),
268     ]
269
270     def _check_unicity(self, cr, uid, ids, context=None):
271         for session in self.browse(cr, uid, ids, context=None):
272             # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user
273             domain = [
274                 ('state', 'not in', ('closed','closing_control')),
275                 ('user_id', '=', uid)
276             ]
277             count = self.search_count(cr, uid, domain, context=context)
278             if count>1:
279                 return False
280         return True
281
282     def _check_pos_config(self, cr, uid, ids, context=None):
283         for session in self.browse(cr, uid, ids, context=None):
284             domain = [
285                 ('state', '!=', 'closed'),
286                 ('config_id', '=', session.config_id.id)
287             ]
288             count = self.search_count(cr, uid, domain, context=context)
289             if count>1:
290                 return False
291         return True
292
293     _constraints = [
294         (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']),
295         (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']),
296     ]
297
298     def create(self, cr, uid, values, context=None):
299         config_id = values.get('config_id', False) or False
300         if config_id:
301             # journal_id is not required on the pos_config because it does not
302             # exists at the installation. If nothing is configured at the
303             # installation we do the minimal configuration. Impossible to do in
304             # the .xml files as the CoA is not yet installed.
305             jobj = self.pool.get('pos.config')
306             pos_config = jobj.browse(cr, uid, config_id, context=context)
307             if not pos_config.journal_id:
308                 jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id']
309                 if jid:
310                     jobj.write(cr, uid, [pos_config.id], {'journal_id': jid}, context=context)
311                 else:
312                     raise osv.except_osv( _('error!'),
313                         _("Unable to open the session. You have to assign a sale journal to your point of sale."))
314
315             # define some cash journal if no payment method exists
316             if not pos_config.journal_ids:
317                 cashids = self.pool.get('account.journal').search(cr, uid, [('journal_user','=',True)], context=context)
318                 if not cashids:
319                     cashids = self.pool.get('account.journal').search(cr, uid, [('type','=','cash')], context=context)
320                     self.pool.get('account.journal').write(cr, uid, cashids, {'journal_user': True})
321                 jobj.write(cr, uid, [pos_config.id], {'journal_ids': [(6,0, cashids)]})
322
323
324             pos_config = jobj.browse(cr, uid, config_id, context=context)
325             bank_statement_ids = []
326             for journal in pos_config.journal_ids:
327                 bank_values = {
328                     'journal_id' : journal.id,
329                     'user_id' : uid,
330                 }
331                 statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context)
332                 bank_statement_ids.append(statement_id)
333
334             values.update({
335                 'name' : pos_config.sequence_id._next(),
336                 'statement_ids' : [(6, 0, bank_statement_ids)],
337                 'config_id': config_id
338             })
339
340         return super(pos_session, self).create(cr, uid, values, context=context)
341
342     def unlink(self, cr, uid, ids, context=None):
343         for obj in self.browse(cr, uid, ids, context=context):
344             for statement in obj.statement_ids:
345                 statement.unlink(context=context)
346         return True
347
348     def wkf_action_open(self, cr, uid, ids, context=None):
349         # second browse because we need to refetch the data from the DB for cash_register_id
350         for record in self.browse(cr, uid, ids, context=context):
351             values = {}
352             if not record.start_at:
353                 values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
354             values['state'] = 'opened'
355             record.write(values, context=context)
356             for st in record.statement_ids:
357                 st.button_open(context=context)
358
359         return self.open_frontend_cb(cr, uid, ids, context=context)
360
361     def wkf_action_opening_control(self, cr, uid, ids, context=None):
362         return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context)
363
364     def wkf_action_closing_control(self, cr, uid, ids, context=None):
365         for session in self.browse(cr, uid, ids, context=context):
366             for statement in session.statement_ids:
367                 if statement != session.cash_register_id and statement.balance_end != statement.balance_end_real:
368                     self.pool.get('account.bank.statement').write(cr, uid, [statement.id], {'balance_end_real': statement.balance_end})
369         return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
370
371     def wkf_action_close(self, cr, uid, ids, context=None):
372         # Close CashBox
373         bsl = self.pool.get('account.bank.statement.line')
374         for record in self.browse(cr, uid, ids, context=context):
375             for st in record.statement_ids:
376                 if abs(st.difference) > st.journal_id.amount_authorized_diff:
377                     # The pos manager can close statements with maximums.
378                     if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"):
379                         raise osv.except_osv( _('Error!'),
380                             _("Your ending balance is too different from the theorical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff))
381                 if st.difference and st.journal_id.cash_control == True:
382                     if st.difference > 0.0:
383                         name= _('Point of Sale Profit')
384                         account_id = st.journal_id.profit_account_id.id
385                     else:
386                         account_id = st.journal_id.loss_account_id.id
387                         name= _('Point of Sale Loss')
388                     if not account_id:
389                         raise osv.except_osv( _('Error!'),
390                         _("Please set your profit and loss accounts on your payment method '%s'. This will allow OpenERP to post the difference of %.2f in your ending balance. To close this session, you can update the 'Closing Cash Control' to avoid any difference.") % (st.journal_id.name,st.difference))
391                     bsl.create(cr, uid, {
392                         'statement_id': st.id,
393                         'amount': st.difference,
394                         'ref': record.name,
395                         'name': name,
396                         'account_id': account_id
397                     }, context=context)
398
399                 if st.journal_id.type == 'bank':
400                     st.write({'balance_end_real' : st.balance_end})
401
402                 getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context)
403         self._confirm_orders(cr, uid, ids, context=context)
404         self.write(cr, uid, ids, {'state' : 'closed'}, context=context)
405
406         obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1]
407         return {
408             'type' : 'ir.actions.client',
409             'name' : 'Point of Sale Menu',
410             'tag' : 'reload',
411             'params' : {'menu_id': obj},
412         }
413
414     def _confirm_orders(self, cr, uid, ids, context=None):
415         wf_service = netsvc.LocalService("workflow")
416
417         for session in self.browse(cr, uid, ids, context=context):
418             order_ids = [order.id for order in session.order_ids if order.state == 'paid']
419
420             move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context)
421
422             self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context)
423
424             for order in session.order_ids:
425                 if order.state != 'paid':
426                     raise osv.except_osv(
427                         _('Error!'),
428                         _("You cannot confirm all orders of this session, because they have not the 'paid' status"))
429                 else:
430                     wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr)
431
432         return True
433
434     def open_frontend_cb(self, cr, uid, ids, context=None):
435         if not context:
436             context = {}
437         if not ids:
438             return {}
439         context.update({'session_id' : ids[0]})
440         return {
441             'type' : 'ir.actions.client',
442             'name' : 'Start Point Of Sale',
443             'tag' : 'pos.ui',
444             'context' : context,
445         }
446
447 class pos_order(osv.osv):
448     _name = "pos.order"
449     _description = "Point of Sale"
450     _order = "id desc"
451
452     def create_from_ui(self, cr, uid, orders, context=None):
453         #_logger.info("orders: %r", orders)
454         order_ids = []
455         for tmp_order in orders:
456             order = tmp_order['data']
457             order_id = self.create(cr, uid, {
458                 'name': order['name'],
459                 'user_id': order['user_id'] or False,
460                 'session_id': order['pos_session_id'],
461                 'lines': order['lines']
462             }, context)
463
464             for payments in order['statement_ids']:
465                 payment = payments[2]
466                 self.add_payment(cr, uid, order_id, {
467                     'amount': payment['amount'] or 0.0,
468                     'payment_date': payment['name'],
469                     'payment_name': payment.get('note', False),
470                     'journal': payment['journal_id']
471                 }, context=context)
472
473             if order['amount_return']:
474                 session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context)
475                 self.add_payment(cr, uid, order_id, {
476                     'amount': -order['amount_return'],
477                     'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'),
478                     'payment_name': _('return'),
479                     'journal': session.cash_journal_id.id
480                 }, context=context)
481             order_ids.append(order_id)
482             wf_service = netsvc.LocalService("workflow")
483             wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
484         return order_ids
485
486     def unlink(self, cr, uid, ids, context=None):
487         for rec in self.browse(cr, uid, ids, context=context):
488             if rec.state not in ('draft','cancel'):
489                 raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.'))
490         return super(pos_order, self).unlink(cr, uid, ids, context=context)
491
492     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
493         if not part:
494             return {'value': {}}
495         pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
496         return {'value': {'pricelist_id': pricelist}}
497
498     def _amount_all(self, cr, uid, ids, name, args, context=None):
499         tax_obj = self.pool.get('account.tax')
500         cur_obj = self.pool.get('res.currency')
501         res = {}
502         for order in self.browse(cr, uid, ids, context=context):
503             res[order.id] = {
504                 'amount_paid': 0.0,
505                 'amount_return':0.0,
506                 'amount_tax':0.0,
507             }
508             val1 = val2 = 0.0
509             cur = order.pricelist_id.currency_id
510             for payment in order.statement_ids:
511                 res[order.id]['amount_paid'] +=  payment.amount
512                 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
513             for line in order.lines:
514                 val1 += line.price_subtotal_incl
515                 val2 += line.price_subtotal
516             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
517             res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
518         return res
519
520     def copy(self, cr, uid, id, default=None, context=None):
521         if not default:
522             default = {}
523         d = {
524             'state': 'draft',
525             'invoice_id': False,
526             'account_move': False,
527             'picking_id': False,
528             'statement_ids': [],
529             'nb_print': 0,
530             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
531         }
532         d.update(default)
533         return super(pos_order, self).copy(cr, uid, id, d, context=context)
534
535     _columns = {
536         'name': fields.char('Order Ref', size=64, required=True, readonly=True),
537         'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
538         'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True),
539         'date_order': fields.datetime('Order Date', readonly=True, select=True),
540         'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the the cash register. It can be a reliever, a student or an interim employee."),
541         'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
542         'amount_total': fields.function(_amount_all, string='Total', multi='all'),
543         'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
544         'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
545         'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
546         'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
547         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
548         'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
549
550         'session_id' : fields.many2one('pos.session', 'Session', 
551                                         #required=True,
552                                         select=1,
553                                         domain="[('state', '=', 'opened')]",
554                                         states={'draft' : [('readonly', False)]},
555                                         readonly=True),
556
557         'state': fields.selection([('draft', 'New'),
558                                    ('cancel', 'Cancelled'),
559                                    ('paid', 'Paid'),
560                                    ('done', 'Posted'),
561                                    ('invoiced', 'Invoiced')],
562                                   'Status', readonly=True),
563
564         'invoice_id': fields.many2one('account.invoice', 'Invoice'),
565         'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
566         'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
567         'note': fields.text('Internal Notes'),
568         'nb_print': fields.integer('Number of Print', readonly=True),
569
570         'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True),
571     }
572
573     def _default_session(self, cr, uid, context=None):
574         so = self.pool.get('pos.session')
575         session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context)
576         return session_ids and session_ids[0] or False
577
578     def _default_pricelist(self, cr, uid, context=None):
579         res = self.pool.get('sale.shop').search(cr, uid, [], context=context)
580         if res:
581             shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context)
582             return shop.pricelist_id and shop.pricelist_id.id or False
583         return False
584
585     _defaults = {
586         'user_id': lambda self, cr, uid, context: uid,
587         'state': 'draft',
588         'name': '/', 
589         'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
590         'nb_print': 0,
591         'session_id': _default_session,
592         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
593         'pricelist_id': _default_pricelist,
594     }
595
596     def create(self, cr, uid, values, context=None):
597         values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order')
598         return super(pos_order, self).create(cr, uid, values, context=context)
599
600     def test_paid(self, cr, uid, ids, context=None):
601         """A Point of Sale is paid when the sum
602         @return: True
603         """
604         for order in self.browse(cr, uid, ids, context=context):
605             if order.lines and not order.amount_total:
606                 return True
607             if (not order.lines) or (not order.statement_ids) or \
608                 (abs(order.amount_total-order.amount_paid) > 0.00001):
609                 return False
610         return True
611
612     def create_picking(self, cr, uid, ids, context=None):
613         """Create a picking for each order and validate it."""
614         picking_obj = self.pool.get('stock.picking')
615         partner_obj = self.pool.get('res.partner')
616         move_obj = self.pool.get('stock.move')
617
618         for order in self.browse(cr, uid, ids, context=context):
619             if not order.state=='draft':
620                 continue
621             addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
622             picking_id = picking_obj.create(cr, uid, {
623                 'origin': order.name,
624                 'partner_id': addr.get('delivery',False),
625                 'type': 'out',
626                 'company_id': order.company_id.id,
627                 'move_type': 'direct',
628                 'note': order.note or "",
629                 'invoice_state': 'none',
630                 'auto_picking': True,
631             }, context=context)
632             self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
633             location_id = order.shop_id.warehouse_id.lot_stock_id.id
634             output_id = order.shop_id.warehouse_id.lot_output_id.id
635
636             for line in order.lines:
637                 if line.product_id and line.product_id.type == 'service':
638                     continue
639                 if line.qty < 0:
640                     location_id, output_id = output_id, location_id
641
642                 move_obj.create(cr, uid, {
643                     'name': line.name,
644                     'product_uom': line.product_id.uom_id.id,
645                     'product_uos': line.product_id.uom_id.id,
646                     'picking_id': picking_id,
647                     'product_id': line.product_id.id,
648                     'product_uos_qty': abs(line.qty),
649                     'product_qty': abs(line.qty),
650                     'tracking_id': False,
651                     'state': 'draft',
652                     'location_id': location_id,
653                     'location_dest_id': output_id,
654                 }, context=context)
655                 if line.qty < 0:
656                     location_id, output_id = output_id, location_id
657
658             wf_service = netsvc.LocalService("workflow")
659             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
660             picking_obj.force_assign(cr, uid, [picking_id], context)
661         return True
662
663     def cancel_order(self, cr, uid, ids, context=None):
664         """ Changes order state to cancel
665         @return: True
666         """
667         stock_picking_obj = self.pool.get('stock.picking')
668         for order in self.browse(cr, uid, ids, context=context):
669             wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr)
670             if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
671                 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
672         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
673         return True
674
675     def add_payment(self, cr, uid, order_id, data, context=None):
676         """Create a new payment for the order"""
677         if not context:
678             context = {}
679         statement_obj = self.pool.get('account.bank.statement')
680         statement_line_obj = self.pool.get('account.bank.statement.line')
681         prod_obj = self.pool.get('product.product')
682         property_obj = self.pool.get('ir.property')
683         curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
684         curr_company = curr_c.id
685         order = self.browse(cr, uid, order_id, context=context)
686         args = {
687             'amount': data['amount'],
688         }
689         if 'payment_date' in data:
690             args['date'] = data['payment_date']
691         args['name'] = order.name
692         if data.get('payment_name', False):
693             args['name'] = args['name'] + ': ' + data['payment_name']
694         account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
695         args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
696                              and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
697         args['partner_id'] = order.partner_id and order.partner_id.id or None
698
699         if not args['account_id']:
700             if not args['partner_id']:
701                 msg = _('There is no receivable account defined to make payment.')
702             else:
703                 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,)
704             raise osv.except_osv(_('Configuration Error!'), msg)
705
706         context.pop('pos_session_id', False)
707
708         try:
709             journal_id = long(data['journal'])
710         except Exception:
711             journal_id = False
712
713         statement_id = False
714         for statement in order.session_id.statement_ids:
715             if statement.journal_id.id == journal_id:
716                 statement_id = statement.id
717                 break
718
719         if not statement_id:
720             raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.'))
721
722         args.update({
723             'statement_id' : statement_id,
724             'pos_statement_id' : order_id,
725             'journal_id' : journal_id,
726             'type' : 'customer',
727             'ref' : order.session_id.name,
728         })
729
730         statement_line_obj.create(cr, uid, args, context=context)
731
732         wf_service = netsvc.LocalService("workflow")
733         wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
734         wf_service.trg_write(uid, 'pos.order', order_id, cr)
735
736         return statement_id
737
738     def refund(self, cr, uid, ids, context=None):
739         """Create a copy of order  for refund order"""
740         clone_list = []
741         line_obj = self.pool.get('pos.order.line')
742         for order in self.browse(cr, uid, ids, context=context):
743             clone_id = self.copy(cr, uid, order.id, {
744                 'name': order.name + ' REFUND',
745             }, context=context)
746             clone_list.append(clone_id)
747
748         for clone in self.browse(cr, uid, clone_list, context=context):
749             for order_line in clone.lines:
750                 line_obj.write(cr, uid, [order_line.id], {
751                     'qty': -order_line.qty
752                 }, context=context)
753
754         new_order = ','.join(map(str,clone_list))
755         abs = {
756             #'domain': "[('id', 'in', ["+new_order+"])]",
757             'name': _('Return Products'),
758             'view_type': 'form',
759             'view_mode': 'form',
760             'res_model': 'pos.order',
761             'res_id':clone_list[0],
762             'view_id': False,
763             'context':context,
764             'type': 'ir.actions.act_window',
765             'nodestroy': True,
766             'target': 'current',
767         }
768         return abs
769
770     def action_invoice_state(self, cr, uid, ids, context=None):
771         return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
772
773     def action_invoice(self, cr, uid, ids, context=None):
774         wf_service = netsvc.LocalService("workflow")
775         inv_ref = self.pool.get('account.invoice')
776         inv_line_ref = self.pool.get('account.invoice.line')
777         product_obj = self.pool.get('product.product')
778         inv_ids = []
779
780         for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
781             if order.invoice_id:
782                 inv_ids.append(order.invoice_id.id)
783                 continue
784
785             if not order.partner_id:
786                 raise osv.except_osv(_('Error!'), _('Please provide a partner for the sale.'))
787
788             acc = order.partner_id.property_account_receivable.id
789             inv = {
790                 'name': order.name,
791                 'origin': order.name,
792                 'account_id': acc,
793                 'journal_id': order.sale_journal.id or None,
794                 'type': 'out_invoice',
795                 'reference': order.name,
796                 'partner_id': order.partner_id.id,
797                 'comment': order.note or '',
798                 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
799             }
800             inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
801             if not inv.get('account_id', None):
802                 inv['account_id'] = acc
803             inv_id = inv_ref.create(cr, uid, inv, context=context)
804
805             self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
806             inv_ids.append(inv_id)
807             for line in order.lines:
808                 inv_line = {
809                     'invoice_id': inv_id,
810                     'product_id': line.product_id.id,
811                     'quantity': line.qty,
812                 }
813                 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
814                 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
815                                                                line.product_id.id,
816                                                                line.product_id.uom_id.id,
817                                                                line.qty, partner_id = order.partner_id.id,
818                                                                fposition_id=order.partner_id.property_account_position.id)['value'])
819                 if line.product_id.description_sale:
820                     inv_line['note'] = line.product_id.description_sale
821                 inv_line['price_unit'] = line.price_unit
822                 inv_line['discount'] = line.discount
823                 inv_line['name'] = inv_name
824                 inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
825                     and [(6, 0, inv_line['invoice_line_tax_id'])] or []
826                 inv_line_ref.create(cr, uid, inv_line, context=context)
827             inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
828             wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
829
830         if not inv_ids: return {}
831
832         mod_obj = self.pool.get('ir.model.data')
833         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
834         res_id = res and res[1] or False
835         return {
836             'name': _('Customer Invoice'),
837             'view_type': 'form',
838             'view_mode': 'form',
839             'view_id': [res_id],
840             'res_model': 'account.invoice',
841             'context': "{'type':'out_invoice'}",
842             'type': 'ir.actions.act_window',
843             'nodestroy': True,
844             'target': 'current',
845             'res_id': inv_ids and inv_ids[0] or False,
846         }
847
848     def create_account_move(self, cr, uid, ids, context=None):
849         return self._create_account_move_line(cr, uid, ids, None, None, context=context)
850
851     def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
852         # Tricky, via the workflow, we only have one id in the ids variable
853         """Create a account move line of order grouped by products or not."""
854         account_move_obj = self.pool.get('account.move')
855         account_move_line_obj = self.pool.get('account.move.line')
856         account_period_obj = self.pool.get('account.period')
857         account_tax_obj = self.pool.get('account.tax')
858         user_proxy = self.pool.get('res.users')
859         property_obj = self.pool.get('ir.property')
860
861         period = account_period_obj.find(cr, uid, context=context)[0]
862
863         #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
864
865         if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
866             raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!'))
867
868         current_company = user_proxy.browse(cr, uid, uid, context=context).company_id
869
870         grouped_data = {}
871         have_to_group_by = session and session.config_id.group_by or False
872
873         def compute_tax(amount, tax, line):
874             if amount > 0:
875                 tax_code_id = tax['base_code_id']
876                 tax_amount = line.price_subtotal * tax['base_sign']
877             else:
878                 tax_code_id = tax['ref_base_code_id']
879                 tax_amount = line.price_subtotal * tax['ref_base_sign']
880
881             return (tax_code_id, tax_amount,)
882
883         for order in self.browse(cr, uid, ids, context=context):
884             if order.account_move:
885                 continue
886             if order.state != 'paid':
887                 continue
888
889             user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id
890
891             group_tax = {}
892             account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
893
894             order_account = order.partner_id and \
895                             order.partner_id.property_account_receivable and \
896                             order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id
897
898             if move_id is None:
899                 # Create an entry for the sale
900                 move_id = account_move_obj.create(cr, uid, {
901                     'ref' : order.name,
902                     'journal_id': order.sale_journal.id,
903                 }, context=context)
904
905             def insert_data(data_type, values):
906                 # if have_to_group_by:
907
908                 sale_journal_id = order.sale_journal.id
909
910                 # 'quantity': line.qty,
911                 # 'product_id': line.product_id.id,
912                 values.update({
913                     'date': order.date_order[:10],
914                     'ref': order.name,
915                     'journal_id' : sale_journal_id,
916                     'period_id' : period,
917                     'move_id' : move_id,
918                     'company_id': user_company and user_company.id or False,
919                 })
920
921                 if data_type == 'product':
922                     key = ('product', values['product_id'],)
923                 elif data_type == 'tax':
924                     key = ('tax', values['tax_code_id'],)
925                 elif data_type == 'counter_part':
926                     key = ('counter_part', values['partner_id'], values['account_id'])
927                 else:
928                     return
929
930                 grouped_data.setdefault(key, [])
931
932                 # if not have_to_group_by or (not grouped_data[key]):
933                 #     grouped_data[key].append(values)
934                 # else:
935                 #     pass
936
937                 if have_to_group_by:
938                     if not grouped_data[key]:
939                         grouped_data[key].append(values)
940                     else:
941                         current_value = grouped_data[key][0]
942                         current_value['quantity'] = current_value.get('quantity', 0.0) +  values.get('quantity', 0.0)
943                         current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
944                         current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
945                         current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
946                 else:
947                     grouped_data[key].append(values)
948
949             # Create an move for each order line
950
951             for line in order.lines:
952                 tax_amount = 0
953                 taxes = [t for t in line.product_id.taxes_id]
954                 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
955
956                 for tax in computed_taxes:
957                     tax_amount += round(tax['amount'], 2)
958                     group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
959
960                     group_tax.setdefault(group_key, 0)
961                     group_tax[group_key] += round(tax['amount'], 2)
962
963                 amount = line.price_subtotal
964
965                 # Search for the income account
966                 if  line.product_id.property_account_income.id:
967                     income_account = line.product_id.property_account_income.id
968                 elif line.product_id.categ_id.property_account_income_categ.id:
969                     income_account = line.product_id.categ_id.property_account_income_categ.id
970                 else:
971                     raise osv.except_osv(_('Error!'), _('Please define income '\
972                         'account for this product: "%s" (id:%d).') \
973                         % (line.product_id.name, line.product_id.id, ))
974
975                 # Empty the tax list as long as there is no tax code:
976                 tax_code_id = False
977                 tax_amount = 0
978                 while computed_taxes:
979                     tax = computed_taxes.pop(0)
980                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
981
982                     # If there is one we stop
983                     if tax_code_id:
984                         break
985
986                 # Create a move for the line
987                 insert_data('product', {
988                     'name': line.product_id.name,
989                     'quantity': line.qty,
990                     'product_id': line.product_id.id,
991                     'account_id': income_account,
992                     'credit': ((amount>0) and amount) or 0.0,
993                     'debit': ((amount<0) and -amount) or 0.0,
994                     'tax_code_id': tax_code_id,
995                     'tax_amount': tax_amount,
996                     'partner_id': order.partner_id and order.partner_id.id or False
997                 })
998
999                 # For each remaining tax with a code, whe create a move line
1000                 for tax in computed_taxes:
1001                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
1002                     if not tax_code_id:
1003                         continue
1004
1005                     insert_data('tax', {
1006                         'name': _('Tax'),
1007                         'product_id':line.product_id.id,
1008                         'quantity': line.qty,
1009                         'account_id': income_account,
1010                         'credit': 0.0,
1011                         'debit': 0.0,
1012                         'tax_code_id': tax_code_id,
1013                         'tax_amount': tax_amount,
1014                     })
1015
1016             # Create a move for each tax group
1017             (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
1018
1019             for key, tax_amount in group_tax.items():
1020                 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
1021                 insert_data('tax', {
1022                     'name': _('Tax') + ' ' + tax.name,
1023                     'quantity': line.qty,
1024                     'product_id': line.product_id.id,
1025                     'account_id': key[account_pos],
1026                     'credit': ((tax_amount>0) and tax_amount) or 0.0,
1027                     'debit': ((tax_amount<0) and -tax_amount) or 0.0,
1028                     'tax_code_id': key[tax_code_pos],
1029                     'tax_amount': tax_amount,
1030                 })
1031
1032             # counterpart
1033             insert_data('counter_part', {
1034                 'name': _("Trade Receivables"), #order.name,
1035                 'account_id': order_account,
1036                 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1037                 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1038                 'partner_id': order.partner_id and order.partner_id.id or False
1039             })
1040
1041             order.write({'state':'done', 'account_move': move_id})
1042
1043         for group_key, group_data in grouped_data.iteritems():
1044             for value in group_data:
1045                 account_move_line_obj.create(cr, uid, value, context=context)
1046
1047         return True
1048
1049     def action_payment(self, cr, uid, ids, context=None):
1050         return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1051
1052     def action_paid(self, cr, uid, ids, context=None):
1053         self.create_picking(cr, uid, ids, context=context)
1054         self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1055         return True
1056
1057     def action_cancel(self, cr, uid, ids, context=None):
1058         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1059         return True
1060
1061     def action_done(self, cr, uid, ids, context=None):
1062         self.create_account_move(cr, uid, ids, context=context)
1063         return True
1064
1065 class account_bank_statement(osv.osv):
1066     _inherit = 'account.bank.statement'
1067     _columns= {
1068         'user_id': fields.many2one('res.users', 'User', readonly=True),
1069     }
1070     _defaults = {
1071         'user_id': lambda self,cr,uid,c={}: uid
1072     }
1073 account_bank_statement()
1074
1075 class account_bank_statement_line(osv.osv):
1076     _inherit = 'account.bank.statement.line'
1077     _columns= {
1078         'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1079     }
1080
1081 account_bank_statement_line()
1082
1083 class pos_order_line(osv.osv):
1084     _name = "pos.order.line"
1085     _description = "Lines of Point of Sale"
1086     _rec_name = "product_id"
1087
1088     def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1089         res = dict([(i, {}) for i in ids])
1090         account_tax_obj = self.pool.get('account.tax')
1091         cur_obj = self.pool.get('res.currency')
1092         for line in self.browse(cr, uid, ids, context=context):
1093             taxes = line.product_id.taxes_id
1094             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1095             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)
1096
1097             cur = line.order_id.pricelist_id.currency_id
1098             res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1099             res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1100         return res
1101
1102     def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1103        context = context or {}
1104        if not product_id:
1105             return {}
1106        if not pricelist:
1107            raise osv.except_osv(_('No Pricelist !'),
1108                _('You have to select a pricelist in the sale form !\n' \
1109                'Please set one before choosing a product.'))
1110
1111        price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1112                product_id, qty or 1.0, partner_id)[pricelist]
1113
1114        result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1115        result['value']['price_unit'] = price
1116        return result
1117
1118     def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1119         result = {}
1120         if not product:
1121             return result
1122         account_tax_obj = self.pool.get('account.tax')
1123         cur_obj = self.pool.get('res.currency')
1124
1125         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1126
1127         taxes = prod.taxes_id
1128         price = price_unit * (1 - (discount or 0.0) / 100.0)
1129         taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1130
1131         result['price_subtotal'] = taxes['total']
1132         result['price_subtotal_incl'] = taxes['total_included']
1133         return {'value': result}
1134
1135     _columns = {
1136         'company_id': fields.many2one('res.company', 'Company', required=True),
1137         'name': fields.char('Line No', size=32, required=True),
1138         'notice': fields.char('Discount Notice', size=128),
1139         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1140         'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1141         'qty': fields.float('Quantity', digits=(16, 2)),
1142         'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1143         'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1144         'discount': fields.float('Discount (%)', digits=(16, 2)),
1145         'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1146         'create_date': fields.datetime('Creation Date', readonly=True),
1147     }
1148
1149     _defaults = {
1150         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1151         'qty': lambda *a: 1,
1152         'discount': lambda *a: 0.0,
1153         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1154     }
1155
1156     def copy_data(self, cr, uid, id, default=None, context=None):
1157         if not default:
1158             default = {}
1159         default.update({
1160             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1161         })
1162         return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1163
1164 class pos_category(osv.osv):
1165     _name = 'pos.category'
1166     _description = "Point of Sale Category"
1167     _order = "sequence, name"
1168     def _check_recursion(self, cr, uid, ids, context=None):
1169         level = 100
1170         while len(ids):
1171             cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1172             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1173             if not level:
1174                 return False
1175             level -= 1
1176         return True
1177
1178     _constraints = [
1179         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1180     ]
1181
1182     def name_get(self, cr, uid, ids, context=None):
1183         if not len(ids):
1184             return []
1185         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1186         res = []
1187         for record in reads:
1188             name = record['name']
1189             if record['parent_id']:
1190                 name = record['parent_id'][1]+' / '+name
1191             res.append((record['id'], name))
1192         return res
1193
1194     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1195         res = self.name_get(cr, uid, ids, context=context)
1196         return dict(res)
1197
1198     def _get_image(self, cr, uid, ids, name, args, context=None):
1199         result = dict.fromkeys(ids, False)
1200         for obj in self.browse(cr, uid, ids, context=context):
1201             result[obj.id] = tools.image_get_resized_images(obj.image)
1202         return result
1203     
1204     def _set_image(self, cr, uid, id, name, value, args, context=None):
1205         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
1206
1207     _columns = {
1208         'name': fields.char('Name', size=64, required=True, translate=True),
1209         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1210         'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1211         'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1212         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1213         
1214         # NOTE:  there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
1215         # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
1216         # In this case, the default image is set by the js code. 
1217
1218         'image': fields.binary("Image",
1219             help="This field holds the image used for the category. "\
1220                  "The image is base64 encoded, and PIL-supported. "\
1221                  "It is limited to a 1024x1024 px image."),
1222         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
1223             string="Medium-sized image", type="binary", multi="_get_image",
1224             store = {
1225                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1226             },
1227             help="Medium-sized image of the category. It is automatically "\
1228                  "resized as a 180x180 px image, with aspect ratio preserved. "\
1229                  "Use this field in form views or some kanban views."),
1230         'image_small': fields.function(_get_image, fnct_inv=_set_image,
1231             string="Smal-sized image", type="binary", multi="_get_image",
1232             store = {
1233                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1234             },
1235             help="Small-sized image of the category. It is automatically "\
1236                  "resized as a 50x50 px image, with aspect ratio preserved. "\
1237                  "Use this field anywhere a small image is required."),
1238     }
1239
1240 import io, StringIO
1241
1242 class ean_wizard(osv.osv_memory):
1243     _name = 'pos.ean_wizard'
1244     _columns = {
1245         'ean13_pattern': fields.char('Reference', size=32, required=True, translate=True),
1246     }
1247     def sanitize_ean13(self, cr, uid, ids, context):
1248         for r in self.browse(cr,uid,ids):
1249             ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern)
1250             m = context.get('active_model')
1251             m_id =  context.get('active_id')
1252             self.pool.get(m).write(cr,uid,[m_id],{'ean13':ean13})
1253         return { 'type' : 'ir.actions.act_window_close' }
1254
1255 class product_product(osv.osv):
1256     _inherit = 'product.product'
1257
1258
1259     #def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1260     #    result = {}
1261     #    for obj in self.browse(cr, uid, ids, context=context):
1262     #        if not obj.product_image:
1263     #            result[obj.id] = False
1264     #            continue
1265
1266     #        image_stream = io.BytesIO(obj.product_image.decode('base64'))
1267     #        img = Image.open(image_stream)
1268     #        img.thumbnail((120, 100), Image.ANTIALIAS)
1269     #        img_stream = StringIO.StringIO()
1270     #        img.save(img_stream, "JPEG")
1271     #        result[obj.id] = img_stream.getvalue().encode('base64')
1272     #    return result
1273
1274     _columns = {
1275         'income_pdt': fields.boolean('Point of Sale Cash In', help="This is a product you can use to put cash into a statement for the point of sale backend."),
1276         'expense_pdt': fields.boolean('Point of Sale Cash Out', 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."),
1277         'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
1278             help="If you want to sell this product through the point of sale, select the category it belongs to."),
1279         'to_weight' : fields.boolean('To Weight', help="This category contains products that should be weighted, mainly used for the self-checkout interface"),
1280     }
1281
1282     def _default_pos_categ_id(self, cr, uid, context=None):
1283         proxy = self.pool.get('ir.model.data')
1284
1285         try:
1286             category_id = proxy.get_object_reference(cr, uid, 'point_of_sale', 'categ_others')[1]
1287         except ValueError:
1288             values = {
1289                 'name' : 'Others',
1290             }
1291             category_id = self.pool.get('pos.category').create(cr, uid, values, context=context)
1292             values = {
1293                 'name' : 'categ_others',
1294                 'model' : 'pos.category',
1295                 'module' : 'point_of_sale',
1296                 'res_id' : category_id,
1297             }
1298             proxy.create(cr, uid, values, context=context)
1299
1300         return category_id
1301
1302     _defaults = {
1303         'to_weight' : False,
1304         'pos_categ_id' : _default_pos_categ_id,
1305     }
1306
1307     def edit_ean(self, cr, uid, ids, context):
1308         return {
1309             'name': _("Assign a Custom EAN"),
1310             'type': 'ir.actions.act_window',
1311             'view_type': 'form',
1312             'view_mode': 'form',
1313             'res_model': 'pos.ean_wizard',
1314             'target' : 'new',
1315             'view_id': False,
1316             'context':context,
1317         }
1318
1319 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: