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