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