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