X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Fpoint_of_sale%2Fpoint_of_sale.py;h=44139f7a499eef9a25caa9362ed3a8725224c08d;hb=d2076aece3171bf40014b1d729856dab198a81db;hp=c59aa86f5c90a635b7a09ba0403d749b35733614;hpb=23ca8aaa38713174a3d8380da4b090e8bb942b44;p=odoo%2Fodoo.git diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index c59aa86..44139f7 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -19,17 +19,20 @@ # ############################################################################## -import time from datetime import datetime from dateutil.relativedelta import relativedelta +from decimal import Decimal import logging -from PIL import Image +import pdb +import time -import netsvc -from osv import fields, osv -from tools.translate import _ -from decimal import Decimal -import decimal_precision as dp +import openerp +from openerp import netsvc, tools +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +import openerp.addons.decimal_precision as dp +import openerp.addons.product.product _logger = logging.getLogger(__name__) @@ -37,87 +40,88 @@ class pos_config(osv.osv): _name = 'pos.config' POS_CONFIG_STATE = [ - ('draft', 'Draft'), ('active', 'Active'), ('inactive', 'Inactive'), ('deprecated', 'Deprecated') ] _columns = { - 'name' : fields.char('Name', size=32, - select=1, - required=True, -# readonly=True, -# states={'draft' : [('readonly', False)]} - ), - 'journal_ids' : fields.many2many('account.journal', - 'pos_config_journal_rel', - 'pos_config_id', - 'journal_id', - 'Payment Methods', - domain="[('journal_user', '=', True )]", -# readonly=True, -# states={'draft' : [('readonly', False)]} - ), + 'name' : fields.char('Point of Sale Name', size=32, select=1, + required=True, help="An internal identification of the point of sale"), + 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', + 'pos_config_id', 'journal_id', 'Available Payment Methods', + domain="[('journal_user', '=', True )]",), 'shop_id' : fields.many2one('sale.shop', 'Shop', - required=True, - select=1, -# readonly=True, -# states={'draft' : [('readonly', False)]} - ), - 'journal_id' : fields.many2one('account.journal', 'Journal', - required=True, - select=1, - domain=[('type', '=', 'sale')], -# readonly=True, -# states={'draft' : [('readonly', False)]} - ), - 'iface_self_checkout' : fields.boolean('Self Checkout Mode'), - 'iface_websql' : fields.boolean('WebSQL (to store data)'), - 'iface_led' : fields.boolean('LED Interface'), + required=True), + 'journal_id' : fields.many2one('account.journal', 'Sale Journal', + domain=[('type', '=', 'sale')], + help="Accounting journal used to post sales entries."), + 'iface_self_checkout' : fields.boolean('Self Checkout Mode', + 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."), 'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'), 'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'), 'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'), - 'iface_barscan' : fields.boolean('BarScan Interface'), 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'), 'iface_print_via_proxy' : fields.boolean('Print via Proxy'), - 'state' : fields.selection(POS_CONFIG_STATE, 'State', - required=True, - readonly=True), + 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True), + 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, + help="This sequence is automatically created by OpenERP but you can change it "\ + "to customize the reference numbers of your orders."), + 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'), + 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), + } - 'sequence_id' : fields.many2one('ir.sequence', 'Sequence', - readonly=True), - 'user_id' : fields.many2one('res.users', 'User', -# readonly=True, -# states={'draft' : [('readonly', False)]} - ), + def _check_cash_control(self, cr, uid, ids, context=None): + return all( + (sum(int(journal.cash_control) for journal in record.journal_ids) <= 1) + for record in self.browse(cr, uid, ids, context=context) + ) - } + _constraints = [ + (_check_cash_control, "You cannot have two cash controls in one Point Of Sale !", ['journal_ids']), + ] - _defaults = { - 'state' : 'draft', - 'user_id' : lambda obj, cr, uid, context: uid, - } + def copy(self, cr, uid, id, default=None, context=None): + if not default: + default = {} + d = { + 'sequence_id' : False, + } + d.update(default) + return super(pos_config, self).copy(cr, uid, id, d, context=context) - def _check_only_one_cash_journal(self, cr, uid, ids, context=None): + + def name_get(self, cr, uid, ids, context=None): + result = [] + states = { + 'opening_control': _('Opening Control'), + 'opened': _('In Progress'), + 'closing_control': _('Closing Control'), + 'closed': _('Closed & Posted'), + } for record in self.browse(cr, uid, ids, context=context): - has_cash_journal = False + if (not record.session_ids) or (record.session_ids[0].state=='closed'): + result.append((record.id, record.name+' ('+_('not used')+')')) + continue + session = record.session_ids[0] + result.append((record.id, record.name + ' ('+session.user_id.name+')')) #, '+states[session.state]+')')) + return result - for journal in record.journal_ids: - if journal.type == 'cash': - if has_cash_journal: - return False - else: - has_cash_journal = True - return True + def _default_sale_journal(self, cr, uid, context=None): + res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1) + return res and res[0] or False - _constraints = [ - (_check_only_one_cash_journal, "You should have only one Cash Journal !", ['journal_id']), - ] + def _default_shop(self, cr, uid, context=None): + res = self.pool.get('sale.shop').search(cr, uid, []) + return res and res[0] or False - def set_draft(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'draft'}, context=context) + _defaults = { + 'state' : POS_CONFIG_STATE[0][0], + 'shop_id': _default_shop, + 'journal_id': _default_sale_journal, + 'group_by' : True, + } def set_active(self, cr, uid, ids, context=None): return self.write(cr, uid, ids, {'state' : 'active'}, context=context) @@ -129,124 +133,126 @@ class pos_config(osv.osv): return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context) def create(self, cr, uid, values, context=None): - proxy = self.pool.get('ir.sequence.type') - - sequence_values = dict( - code='pos_%s_sequence' % values['name'].lower(), - name='POS %s Sequence' % values['name'], - ) - - proxy.create(cr, uid, sequence_values, context=context) - proxy = self.pool.get('ir.sequence') - sequence_values = dict( - code='pos_%s_sequence' % values['name'].lower(), - name='POS %s Sequence' % values['name'], - padding=4, - prefix="%s/%%(year)s/%%(month)s/%%(day)s/" % values['name'], + name='PoS %s' % values['name'], + padding=5, + prefix="%s/" % values['name'], ) sequence_id = proxy.create(cr, uid, sequence_values, context=context) - values['sequence_id'] = sequence_id return super(pos_config, self).create(cr, uid, values, context=context) - def write(self, cr, uid, ids, values, context=None): - for obj in self.browse(cr, uid, ids, context=context): - if obj.sequence_id and values.get('name', False): - prefixes = obj.sequence_id.prefix.split('/') - if len(prefixes) >= 4 and prefixes[0] == obj.name: - prefixes[0] = values['name'] - - sequence_values = dict( - code='pos_%s_sequence' % values['name'].lower(), - name='POS %s Sequence' % values['name'], - prefix="/".join(prefixes), - ) - obj.sequence_id.write(sequence_values) - - return super(pos_config, self).write(cr, uid, ids, values, context=context) - def unlink(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): if obj.sequence_id: obj.sequence_id.unlink() - return super(pos_config, self).unlink(cr, uid, ids, context=context) -pos_config() - class pos_session(osv.osv): _name = 'pos.session' + _order = 'id desc' POS_SESSION_STATE = [ ('opening_control', 'Opening Control'), # Signal open - ('opened', 'Opened'), # Signal closing + ('opened', 'In Progress'), # Signal closing ('closing_control', 'Closing Control'), # Signal close - ('closed', 'Closed'), + ('closed', 'Closed & Posted'), ] - def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, False) + def _compute_cash_all(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + for record in self.browse(cr, uid, ids, context=context): - cash_register_id = False - for bank_statement in record.statement_ids: - if bank_statement.journal_id.type == 'cash': - cash_register_id = bank_statement.id - break - result[record.id] = cash_register_id + result[record.id] = { + 'cash_journal_id' : False, + 'cash_register_id' : False, + 'cash_control' : False, + } + for st in record.statement_ids: + if st.journal_id.cash_control == True: + result[record.id]['cash_control'] = True + result[record.id]['cash_journal_id'] = st.journal_id.id + result[record.id]['cash_register_id'] = st.id return result _columns = { - 'config_id' : fields.many2one('pos.config', 'PoS', + 'config_id' : fields.many2one('pos.config', 'Point of Sale', + help="The physical point of sale you will use.", required=True, select=1, domain="[('state', '=', 'active')]", -# readonly=True, -# states={'draft' : [('readonly', False)]} ), - 'name' : fields.char('Session Sequence', size=32, - required=True, - select=1, -# readonly=True, -# states={'draft' : [('readonly', False)]} - ), - 'user_id' : fields.many2one('res.users', 'User', + 'name' : fields.char('Session ID', size=32, required=True, readonly=True), + 'user_id' : fields.many2one('res.users', 'Responsible', required=True, select=1, -# readonly=True, -# states={'draft' : [('readonly', False)]} + readonly=True, + states={'opening_control' : [('readonly', False)]} ), - 'start_at' : fields.datetime('Opening Date'), - 'stop_at' : fields.datetime('Closing Date'), - - 'state' : fields.selection(POS_SESSION_STATE, 'State', - required=True, - readonly=True, - select=1), - - 'cash_register_id' : fields.function(_compute_cash_register_id, method=True, + 'start_at' : fields.datetime('Opening Date', readonly=True), + 'stop_at' : fields.datetime('Closing Date', readonly=True), + + 'state' : fields.selection(POS_SESSION_STATE, 'Status', + required=True, readonly=True, + select=1), + + 'cash_control' : fields.function(_compute_cash_all, + multi='cash', + type='boolean', string='Has Cash Control'), + 'cash_journal_id' : fields.function(_compute_cash_all, + multi='cash', + type='many2one', relation='account.journal', + string='Cash Journal', store=True), + 'cash_register_id' : fields.function(_compute_cash_all, + multi='cash', type='many2one', relation='account.bank.statement', string='Cash Register', store=True), + 'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids', + type='one2many', relation='account.cashbox.line', + string='Opening Cash Control'), 'details_ids' : fields.related('cash_register_id', 'details_ids', - type='one2many', relation='account.cashbox.line', - string='CashBox Lines'), + type='one2many', relation='account.cashbox.line', + string='Cash Control'), + + 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real', + type='float', + digits_compute=dp.get_precision('Account'), + string="Ending Balance", + help="Computed using the cash control lines", + readonly=True), + 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start', + type='float', + digits_compute=dp.get_precision('Account'), + string="Starting Balance", + help="Computed using the cash control at the opening.", + readonly=True), + 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding', + string='Total Cash Transaction', + readonly=True), + 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end', + type='float', + digits_compute=dp.get_precision('Account'), + string="Computed Balance", + help="Computed with the initial cash control and the sum of all payments.", + readonly=True), + 'cash_register_difference' : fields.related('cash_register_id', 'difference', + type='float', + string='Difference', + help="Difference between the counted cash control at the closing and the computed balance.", + readonly=True), + 'journal_ids' : fields.related('config_id', 'journal_ids', type='many2many', readonly=True, relation='account.journal', - string='Journals'), + string='Available Payment Methods'), 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'), - 'statement_ids' : fields.many2many('account.bank.statement', - 'pos_session_statement_rel', - 'session_id', - 'statement_id', - 'Bank Statement', - readonly=True), + 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True), } _defaults = { @@ -259,28 +265,84 @@ class pos_session(osv.osv): ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"), ] - def create(self, cr, uid, values, context=None): - config_id = values.get('config_id', False) or False - - pos_config = None - if config_id: - pos_config = self.pool.get('pos.config').browse(cr, uid, config_id, context=context) + def _check_unicity(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=None): + # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user + domain = [ + ('state', 'not in', ('closed','closing_control')), + ('user_id', '=', uid) + ] + count = self.search_count(cr, uid, domain, context=context) + if count>1: + return False + return True - bank_statement_ids = [] - for journal in pos_config.journal_ids: - bank_values = { - 'journal_id' : journal.id, - 'user_id' : pos_config.user_id and pos_config.user_id.id or uid, - } + def _check_pos_config(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=None): + domain = [ + ('state', '!=', 'closed'), + ('config_id', '=', session.config_id.id) + ] + count = self.search_count(cr, uid, domain, context=context) + if count>1: + return False + return True - statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context) + _constraints = [ + (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']), + (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']), + ] - bank_statement_ids.append(statement_id) + def create(self, cr, uid, values, context=None): + context = context or {} + config_id = values.get('config_id', False) or context.get('default_config_id', False) + if not config_id: + raise osv.except_osv( _('Error!'), + _("You should assign a Point of Sale to your session.")) + + # journal_id is not required on the pos_config because it does not + # exists at the installation. If nothing is configured at the + # installation we do the minimal configuration. Impossible to do in + # the .xml files as the CoA is not yet installed. + jobj = self.pool.get('pos.config') + pos_config = jobj.browse(cr, uid, config_id, context=context) + context.update({'company_id': pos_config.shop_id.company_id.id}) + if not pos_config.journal_id: + jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id'] + if jid: + jobj.write(cr, uid, [pos_config.id], {'journal_id': jid}, context=context) + else: + raise osv.except_osv( _('error!'), + _("Unable to open the session. You have to assign a sale journal to your point of sale.")) + + # define some cash journal if no payment method exists + if not pos_config.journal_ids: + journal_proxy = self.pool.get('account.journal') + cashids = journal_proxy.search(cr, uid, [('journal_user', '=', True), ('type','=','cash')], context=context) + if not cashids: + cashids = journal_proxy.search(cr, uid, [('type', '=', 'cash')], context=context) + if not cashids: + cashids = journal_proxy.search(cr, uid, [('journal_user','=',True)], context=context) + + jobj.write(cr, uid, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) + + + pos_config = jobj.browse(cr, uid, config_id, context=context) + bank_statement_ids = [] + for journal in pos_config.journal_ids: + bank_values = { + 'journal_id' : journal.id, + 'user_id' : uid, + 'company_id' : pos_config.shop_id.company_id.id + } + statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context) + bank_statement_ids.append(statement_id) - values.update({ - 'name' : pos_config.sequence_id._next(), - 'statement_ids' : [(6, 0, bank_statement_ids)] - }) + values.update({ + 'name' : pos_config.sequence_id._next(), + 'statement_ids' : [(6, 0, bank_statement_ids)], + 'config_id': config_id + }) return super(pos_session, self).create(cr, uid, values, context=context) @@ -290,111 +352,127 @@ class pos_session(osv.osv): statement.unlink(context=context) return True - def on_change_config(self, cr, uid, ids, config_id, context=None): - result = dict(value=dict()) - if not config_id: - result['value']['user_id'] = uid - else: - result['value']['user_id'] = self.pool.get('pos.config').browse(cr, uid, config_id, context=context).user_id.id - return result + def open_cb(self, cr, uid, ids, context=None): + """ + call the Point Of Sale interface and set the pos.session to 'opened' (in progress) + """ + if context is None: + context = dict() + + if isinstance(ids, (int, long)): + ids = [ids] + + this_record = self.browse(cr, uid, ids[0], context=context) + this_record._workflow_signal('open') + + context.update(active_id=this_record.id) + + return { + 'type' : 'ir.actions.client', + 'name' : _('Start Point Of Sale'), + 'tag' : 'pos.ui', + 'context' : context, + } def wkf_action_open(self, cr, uid, ids, context=None): - # si pas de date start_at, je balance une date, sinon on utilise celle de l'utilisateur + # second browse because we need to refetch the data from the DB for cash_register_id for record in self.browse(cr, uid, ids, context=context): values = {} if not record.start_at: values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S') values['state'] = 'opened' - record.write(values, context=context) - for st in record.statement_ids: st.button_open(context=context) - return True + return self.open_frontend_cb(cr, uid, ids, context=context) + + def wkf_action_opening_control(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context) def wkf_action_closing_control(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=context): + for statement in session.statement_ids: + if (statement != session.cash_register_id) and (statement.balance_end != statement.balance_end_real): + self.pool.get('account.bank.statement').write(cr, uid, [statement.id], {'balance_end_real': statement.balance_end}) + return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) + + def wkf_action_close(self, cr, uid, ids, context=None): # Close CashBox + bsl = self.pool.get('account.bank.statement.line') for record in self.browse(cr, uid, ids, context=context): for st in record.statement_ids: - getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context) + if abs(st.difference) > st.journal_id.amount_authorized_diff: + # The pos manager can close statements with maximums. + if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"): + raise osv.except_osv( _('Error!'), + _("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)) + if st.difference and st.journal_id.cash_control == True: + if st.difference > 0.0: + name= _('Point of Sale Profit') + account_id = st.journal_id.profit_account_id.id + else: + account_id = st.journal_id.loss_account_id.id + name= _('Point of Sale Loss') + if not account_id: + raise osv.except_osv( _('Error!'), + _("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)) + bsl.create(cr, uid, { + 'statement_id': st.id, + 'amount': st.difference, + 'ref': record.name, + 'name': name, + 'account_id': account_id + }, context=context) - return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) + if st.journal_id.type == 'bank': + st.write({'balance_end_real' : st.balance_end}) - def wkf_action_close(self, cr, uid, ids, context=None): + getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context) self._confirm_orders(cr, uid, ids, context=context) - return self.write(cr, uid, ids, {'state' : 'closed'}, context=context) + self.write(cr, uid, ids, {'state' : 'closed'}, context=context) + + obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1] + return { + 'type' : 'ir.actions.client', + 'name' : 'Point of Sale Menu', + 'tag' : 'reload', + 'params' : {'menu_id': obj}, + } def _confirm_orders(self, cr, uid, ids, context=None): wf_service = netsvc.LocalService("workflow") for session in self.browse(cr, uid, ids, context=context): + order_ids = [order.id for order in session.order_ids if order.state == 'paid'] + + move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context) + + self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context) + for order in session.order_ids: - if order.state != 'paid': + if order.state not in ('paid', 'invoiced'): raise osv.except_osv( - _('Error !'), - _("You can not confirm all orders of this session, because they have not the 'paid' status")) + _('Error!'), + _("You cannot confirm all orders of this session, because they have not the 'paid' status")) else: wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr) return True - def get_current_session(self, cr, uid, context=None): - current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - domain = [ - ('state', '=', 'open'), - ('start_at', '>=', time.strftime('%Y-%m-%d 00:00:00')), - ('user_id', '=', uid), - ] - session_ids = self.search(cr, uid, domain, context=context, limit=1, order='start_at desc') - session_id = session_ids[0] if session_ids else False - - if not session_id: - pos_config_proxy = self.pool.get('pos.config') - domain = [ - ('user_id', '=', uid), - ('state', '=', 'active'), - ] - pos_config_ids = pos_config_proxy.search(cr, uid, domain, - limit=1, - order='create_date desc', - context=context) - - if not pos_config_ids: - raise osv.except_osv(_('Error !'), - _('There is no active PoS Config for this User %s') % current_user.name) - - config = pos_config_proxy.browse(cr, uid, pos_config_ids[0], context=context) - - values = { - 'state' : 'new', - 'start_at' : time.strftime('%Y-%m-%d %H:%M:%S'), - 'config_id' : config.id, - 'journal_id' : config.journal_id.id, - 'user_id': current_user.id, - } - - session_id = self.create(cr, uid, values, context=context) - wkf_service = netsvc.LocalService('workflow') - wkf_service.trg_validate(uid, 'pos.session', session_id, 'opening_control', cr) - - return session_id - -pos_session() - -class pos_config_journal(osv.osv): - """ Point of Sale journal configuration""" - _name = 'pos.config.journal' - _description = "Journal Configuration" - - _columns = { - 'name': fields.char('Description', size=64), - 'code': fields.char('Code', size=64), - 'journal_id': fields.many2one('account.journal', "Journal") - } - -pos_config_journal() + def open_frontend_cb(self, cr, uid, ids, context=None): + if not context: + context = {} + if not ids: + return {} + context.update({'active_id': ids[0]}) + return { + 'type' : 'ir.actions.client', + 'name' : _('Start Point Of Sale'), + 'tag' : 'pos.ui', + 'context' : context, + } class pos_order(osv.osv): _name = "pos.order" @@ -403,29 +481,47 @@ class pos_order(osv.osv): def create_from_ui(self, cr, uid, orders, context=None): #_logger.info("orders: %r", orders) - list = [] - session_id = self.pool.get('pos.session').get_current_session(cr, uid, context=context) - for order in orders: - # 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} - order['session_id'] = session_id - order_obj = self.pool.get('pos.order') - # get statements out of order because they will be generated with add_payment to ensure - # the module behavior is the same when using the front-end or the back-end - if not order['data']['statement_ids']: - continue - statement_ids = order['data'].pop('statement_ids') - order_id = self.create(cr, uid, order, context) - list.append(order_id) - # call add_payment; refer to wizard/pos_payment for data structure - # add_payment launches the 'paid' signal to advance the workflow to the 'paid' state - data = { - 'journal': statement_ids[0][2]['journal_id'], - 'amount': order['data']['amount_paid'], - 'payment_name': order['data']['name'], - 'payment_date': statement_ids[0][2]['name'], - } - order_obj.add_payment(cr, uid, order_id, data, context=context) - return list + order_ids = [] + for tmp_order in orders: + order = tmp_order['data'] + order_id = self.create(cr, uid, { + 'name': order['name'], + 'user_id': order['user_id'] or False, + 'session_id': order['pos_session_id'], + 'lines': order['lines'], + 'pos_reference':order['name'] + }, context) + + for payments in order['statement_ids']: + payment = payments[2] + self.add_payment(cr, uid, order_id, { + 'amount': payment['amount'] or 0.0, + 'payment_date': payment['name'], + 'statement_id': payment['statement_id'], + 'payment_name': payment.get('note', False), + 'journal': payment['journal_id'] + }, context=context) + + if order['amount_return']: + session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context) + cash_journal = session.cash_journal_id + cash_statement = False + if not cash_journal: + cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids) + if not len(cash_journal_ids): + raise osv.except_osv( _('error!'), + _("No cash statement found for this session. Unable to record returned cash.")) + cash_journal = cash_journal_ids[0].journal_id + self.add_payment(cr, uid, order_id, { + 'amount': -order['amount_return'], + 'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'), + 'payment_name': _('return'), + 'journal': cash_journal.id, + }, context=context) + order_ids.append(order_id) + wf_service = netsvc.LocalService("workflow") + wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr) + return order_ids def unlink(self, cr, uid, ids, context=None): for rec in self.browse(cr, uid, ids, context=context): @@ -461,14 +557,6 @@ class pos_order(osv.osv): res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1) return res - def _default_sale_journal(self, cr, uid, context=None): - res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1) - return res and res[0] or False - - def _default_shop(self, cr, uid, context=None): - res = self.pool.get('sale.shop').search(cr, uid, []) - return res and res[0] or False - def copy(self, cr, uid, id, default=None, context=None): if not default: default = {} @@ -487,10 +575,9 @@ class pos_order(osv.osv): _columns = { 'name': fields.char('Order Ref', size=64, required=True, readonly=True), 'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True), - 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, - states={'draft': [('readonly', False)]}, readonly=True), - 'date_order': fields.datetime('Date Ordered', readonly=True, select=True), - 'user_id': fields.many2one('res.users', 'Connected Salesman', help="Person who uses the the cash register. It could be a reliever, a student or an interim employee."), + 'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True), + 'date_order': fields.datetime('Order Date', readonly=True, select=True), + '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."), 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'), 'amount_total': fields.function(_amount_all, string='Total', multi='all'), 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'), @@ -512,20 +599,27 @@ class pos_order(osv.osv): ('paid', 'Paid'), ('done', 'Posted'), ('invoiced', 'Invoiced')], - 'State', readonly=True), + 'Status', readonly=True), 'invoice_id': fields.many2one('account.invoice', 'Invoice'), 'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True), 'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True), 'note': fields.text('Internal Notes'), 'nb_print': fields.integer('Number of Print', readonly=True), - 'sale_journal': fields.many2one('account.journal', 'Journal', required=True, states={'draft': [('readonly', False)]}, readonly=True), + 'pos_reference': fields.char('Receipt Ref', size=64, readonly=True), + 'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True), } + def _default_session(self, cr, uid, context=None): + so = self.pool.get('pos.session') + session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context) + return session_ids and session_ids[0] or False + def _default_pricelist(self, cr, uid, context=None): - res = self.pool.get('sale.shop').search(cr, uid, [], context=context) - if res: - shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context) + session_ids = self._default_session(cr, uid, context) + if session_ids: + session_record = self.pool.get('pos.session').browse(cr, uid, session_ids, context=context) + shop = self.pool.get('sale.shop').browse(cr, uid, session_record.config_id.shop_id.id, context=context) return shop.pricelist_id and shop.pricelist_id.id or False return False @@ -535,9 +629,8 @@ class pos_order(osv.osv): 'name': '/', 'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), 'nb_print': 0, + 'session_id': _default_session, 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - 'sale_journal': _default_sale_journal, - 'shop_id': _default_shop, 'pricelist_id': _default_pricelist, } @@ -608,23 +701,12 @@ class pos_order(osv.osv): picking_obj.force_assign(cr, uid, [picking_id], context) return True - def set_to_draft(self, cr, uid, ids, *args): - if not len(ids): - return False - for order in self.browse(cr, uid, ids, context=context): - if order.state != 'cancel': - raise osv.except_osv(_('Error!'), _('In order to set to draft a sale, it must be cancelled.')) - self.write(cr, uid, ids, {'state': 'draft'}) - wf_service = netsvc.LocalService("workflow") - for i in ids: - wf_service.trg_create(uid, 'pos.order', i, cr) - return True - def cancel_order(self, cr, uid, ids, context=None): """ Changes order state to cancel @return: True """ stock_picking_obj = self.pool.get('stock.picking') + wf_service = netsvc.LocalService("workflow") for order in self.browse(cr, uid, ids, context=context): wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr) if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel': @@ -636,22 +718,15 @@ class pos_order(osv.osv): """Create a new payment for the order""" if not context: context = {} - statement_obj = self.pool.get('account.bank.statement') statement_line_obj = self.pool.get('account.bank.statement.line') - prod_obj = self.pool.get('product.product') property_obj = self.pool.get('ir.property') - curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id - curr_company = curr_c.id order = self.browse(cr, uid, order_id, context=context) - ids_new = [] args = { 'amount': data['amount'], + 'date': data.get('payment_date', time.strftime('%Y-%m-%d')), + 'name': order.name + ': ' + (data.get('payment_name', '') or ''), } - if 'payment_date' in data.keys(): - args['date'] = data['payment_date'] - args['name'] = order.name - if data.get('payment_name', False): - args['name'] = args['name'] + ': ' + data['payment_name'] + account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context) args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \ and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False @@ -659,41 +734,37 @@ class pos_order(osv.osv): if not args['account_id']: if not args['partner_id']: - msg = _('There is no receivable account defined to make payment') + msg = _('There is no receivable account defined to make payment.') else: - msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d)') % (order.partner_id.name, order.partner_id.id,) - raise osv.except_osv(_('Configuration Error !'), msg) + msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,) + raise osv.except_osv(_('Configuration Error!'), msg) context.pop('pos_session_id', False) - try: - journal_id = long(data['journal']) - except Exception: - journal_id = False + journal_id = data.get('journal', False) + statement_id = data.get('statement_id', False) + assert journal_id or statement_id, "No statement_id or journal_id passed to the method!" - statement_id = False for statement in order.session_id.statement_ids: - if statement.journal_id.id == journal_id: + if statement.id == statement_id: + journal_id = statement.journal_id.id + break + elif statement.journal_id.id == journal_id: statement_id = statement.id break if not statement_id: - raise osv.except_osv(_('Error !'), _('You have to open at least one cashbox')) + raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.')) args.update({ 'statement_id' : statement_id, 'pos_statement_id' : order_id, 'journal_id' : journal_id, 'type' : 'customer', - 'ref' : order.name, + 'ref' : order.session_id.name, }) statement_line_obj.create(cr, uid, args, context=context) - ids_new.append(statement_id) - - wf_service = netsvc.LocalService("workflow") - wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr) - wf_service.trg_write(uid, 'pos.order', order_id, cr) return statement_id @@ -745,7 +816,7 @@ class pos_order(osv.osv): continue if not order.partner_id: - raise osv.except_osv(_('Error'), _('Please provide a partner for the sale.')) + raise osv.except_osv(_('Error!'), _('Please provide a partner for the sale.')) acc = order.partner_id.property_account_receivable.id inv = { @@ -783,8 +854,7 @@ class pos_order(osv.osv): inv_line['price_unit'] = line.price_unit inv_line['discount'] = line.discount inv_line['name'] = inv_name - inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\ - and [(6, 0, inv_line['invoice_line_tax_id'])] or [] + inv_line['invoice_line_tax_id'] = [(6, 0, [x.id for x in line.product_id.taxes_id] )] inv_line_ref.create(cr, uid, inv_line, context=context) inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context) wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr) @@ -808,51 +878,124 @@ class pos_order(osv.osv): } def create_account_move(self, cr, uid, ids, context=None): + return self._create_account_move_line(cr, uid, ids, None, None, context=context) + + def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None): + # Tricky, via the workflow, we only have one id in the ids variable """Create a account move line of order grouped by products or not.""" account_move_obj = self.pool.get('account.move') account_move_line_obj = self.pool.get('account.move.line') account_period_obj = self.pool.get('account.period') - period = account_period_obj.find(cr, uid, context=context)[0] account_tax_obj = self.pool.get('account.tax') - res_obj=self.pool.get('res.users') - property_obj=self.pool.get('ir.property') + user_proxy = self.pool.get('res.users') + property_obj = self.pool.get('ir.property') + + period = account_period_obj.find(cr, uid, context=context)[0] + + #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context)) + + if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)): + raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!')) + + current_company = user_proxy.browse(cr, uid, uid, context=context).company_id + + grouped_data = {} + have_to_group_by = session and session.config_id.group_by or False + + def compute_tax(amount, tax, line): + if amount > 0: + tax_code_id = tax['base_code_id'] + tax_amount = line.price_subtotal * tax['base_sign'] + else: + tax_code_id = tax['ref_base_code_id'] + tax_amount = line.price_subtotal * tax['ref_base_sign'] + + return (tax_code_id, tax_amount,) for order in self.browse(cr, uid, ids, context=context): + if order.account_move: + continue if order.state != 'paid': continue - curr_c = res_obj.browse(cr, uid, uid).company_id - comp_id = res_obj.browse(cr, order.user_id.id, order.user_id.id).company_id - comp_id = comp_id and comp_id.id or False - to_reconcile = [] + user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id + group_tax = {} account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id - order_account = order.partner_id and order.partner_id.property_account_receivable and order.partner_id.property_account_receivable.id or account_def or curr_c.account_receivable.id + order_account = order.partner_id and \ + order.partner_id.property_account_receivable and \ + order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id - # Create an entry for the sale - move_id = account_move_obj.create(cr, uid, { - 'ref' : order.name, - 'journal_id': order.sale_journal.id, - }, context=context) + if move_id is None: + # Create an entry for the sale + move_id = account_move_obj.create(cr, uid, { + 'ref' : order.name, + 'journal_id': order.sale_journal.id, + }, context=context) + + def insert_data(data_type, values): + # if have_to_group_by: + sale_journal_id = order.sale_journal.id + + # 'quantity': line.qty, + # 'product_id': line.product_id.id, + values.update({ + 'date': order.date_order[:10], + 'ref': order.name, + 'journal_id' : sale_journal_id, + 'period_id' : period, + 'move_id' : move_id, + 'company_id': user_company and user_company.id or False, + }) + + if data_type == 'product': + key = ('product', values['product_id'],) + elif data_type == 'tax': + key = ('tax', values['tax_code_id'],) + elif data_type == 'counter_part': + key = ('counter_part', values['partner_id'], values['account_id']) + else: + return + + grouped_data.setdefault(key, []) + + # if not have_to_group_by or (not grouped_data[key]): + # grouped_data[key].append(values) + # else: + # pass + + if have_to_group_by: + if not grouped_data[key]: + grouped_data[key].append(values) + else: + current_value = grouped_data[key][0] + current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) + current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) + current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) + current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0) + else: + grouped_data[key].append(values) + + #because of the weird way the pos order is written, we need to make sure there is at least one line, + #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that + #are set inside the for loop) + #TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack + assert order.lines, _('The POS order must have lines when calling this method') # Create an move for each order line for line in order.lines: tax_amount = 0 taxes = [t for t in line.product_id.taxes_id] - computed = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty) - computed_taxes = computed['taxes'] + computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes'] for tax in computed_taxes: tax_amount += round(tax['amount'], 2) - group_key = (tax['tax_code_id'], - tax['base_code_id'], - tax['account_collected_id']) + group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id']) + + group_tax.setdefault(group_key, 0) + group_tax[group_key] += round(tax['amount'], 2) - if group_key in group_tax: - group_tax[group_key] += round(tax['amount'], 2) - else: - group_tax[group_key] = round(tax['amount'], 2) amount = line.price_subtotal # Search for the income account @@ -861,8 +1004,8 @@ class pos_order(osv.osv): elif line.product_id.categ_id.property_account_income_categ.id: income_account = line.product_id.categ_id.property_account_income_categ.id else: - raise osv.except_osv(_('Error !'), _('There is no income '\ - 'account defined for this product: "%s" (id:%d)') \ + raise osv.except_osv(_('Error!'), _('Please define income '\ + 'account for this product: "%s" (id:%d).') \ % (line.product_id.name, line.product_id.id, )) # Empty the tax list as long as there is no tax code: @@ -870,102 +1013,73 @@ class pos_order(osv.osv): tax_amount = 0 while computed_taxes: tax = computed_taxes.pop(0) - if amount > 0: - tax_code_id = tax['base_code_id'] - tax_amount = line.price_subtotal * tax['base_sign'] - else: - tax_code_id = tax['ref_base_code_id'] - tax_amount = line.price_subtotal * tax['ref_base_sign'] + tax_code_id, tax_amount = compute_tax(amount, tax, line) + # If there is one we stop if tax_code_id: break # Create a move for the line - account_move_line_obj.create(cr, uid, { + insert_data('product', { 'name': line.product_id.name, - 'date': order.date_order[:10], - 'ref': order.name, 'quantity': line.qty, 'product_id': line.product_id.id, - 'move_id': move_id, 'account_id': income_account, - 'company_id': comp_id, 'credit': ((amount>0) and amount) or 0.0, 'debit': ((amount<0) and -amount) or 0.0, - 'journal_id': order.sale_journal.id, - 'period_id': period, 'tax_code_id': tax_code_id, 'tax_amount': tax_amount, 'partner_id': order.partner_id and order.partner_id.id or False - }, context=context) + }) # For each remaining tax with a code, whe create a move line for tax in computed_taxes: - if amount > 0: - tax_code_id = tax['base_code_id'] - tax_amount = line.price_subtotal * tax['base_sign'] - else: - tax_code_id = tax['ref_base_code_id'] - tax_amount = line.price_subtotal * tax['ref_base_sign'] + tax_code_id, tax_amount = compute_tax(amount, tax, line) if not tax_code_id: continue - account_move_line_obj.create(cr, uid, { - 'name': "Tax" + line.name + " (%s)" % (tax.name), - 'date': order.date_order[:10], - 'ref': order.name, + insert_data('tax', { + 'name': _('Tax'), 'product_id':line.product_id.id, 'quantity': line.qty, - 'move_id': move_id, 'account_id': income_account, - 'company_id': comp_id, 'credit': 0.0, 'debit': 0.0, - 'journal_id': order.sale_journal.id, - 'period_id': period, 'tax_code_id': tax_code_id, 'tax_amount': tax_amount, - }, context=context) - + }) # Create a move for each tax group - (tax_code_pos, base_code_pos, account_pos)= (0, 1, 2) - for key, amount in group_tax.items(): - account_move_line_obj.create(cr, uid, { - 'name': 'Tax', - 'date': order.date_order[:10], - 'ref': order.name, - 'move_id': move_id, - 'company_id': comp_id, + (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3) + + for key, tax_amount in group_tax.items(): + tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context) + insert_data('tax', { + 'name': _('Tax') + ' ' + tax.name, 'quantity': line.qty, 'product_id': line.product_id.id, - 'account_id': key[account_pos], - 'credit': ((amount>0) and amount) or 0.0, - 'debit': ((amount<0) and -amount) or 0.0, - 'journal_id': order.sale_journal.id, - 'period_id': period, + 'account_id': key[account_pos] or income_account, + 'credit': ((tax_amount>0) and tax_amount) or 0.0, + 'debit': ((tax_amount<0) and -tax_amount) or 0.0, 'tax_code_id': key[tax_code_pos], - 'tax_amount': amount, - }, context=context) + 'tax_amount': tax_amount, + }) # counterpart - to_reconcile.append(account_move_line_obj.create(cr, uid, { - 'name': "Trade Receivables", #order.name, - 'date': order.date_order[:10], - 'ref': order.name, - 'move_id': move_id, - 'company_id': comp_id, + insert_data('counter_part', { + 'name': _("Trade Receivables"), #order.name, 'account_id': order_account, - 'credit': ((order.amount_total < 0) and -order.amount_total)\ - or 0.0, - 'debit': ((order.amount_total > 0) and order.amount_total)\ - or 0.0, - 'journal_id': order.sale_journal.id, - 'period_id': period, + 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0, + 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0, 'partner_id': order.partner_id and order.partner_id.id or False - }, context=context)) + }) + + order.write({'state':'done', 'account_move': move_id}) + + for group_key, group_data in grouped_data.iteritems(): + for value in group_data: + account_move_line_obj.create(cr, uid, value, context=context) - self.write(cr, uid, order.id, {'state':'done', 'account_move': move_id}, context=context) return True def action_payment(self, cr, uid, ids, context=None): @@ -984,8 +1098,6 @@ class pos_order(osv.osv): self.create_account_move(cr, uid, ids, context=context) return True -pos_order() - class account_bank_statement(osv.osv): _inherit = 'account.bank.statement' _columns= { @@ -1001,6 +1113,7 @@ class account_bank_statement_line(osv.osv): _columns= { 'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'), } + account_bank_statement_line() class pos_order_line(osv.osv): @@ -1013,9 +1126,9 @@ class pos_order_line(osv.osv): account_tax_obj = self.pool.get('account.tax') cur_obj = self.pool.get('res.currency') for line in self.browse(cr, uid, ids, context=context): - taxes = line.product_id.taxes_id + taxes_ids = [ tax for tax in line.product_id.taxes_id if tax.company_id.id == line.order_id.company_id.id ] price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - 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) + taxes = account_tax_obj.compute_all(cr, uid, taxes_ids, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False) cur = line.order_id.pricelist_id.currency_id res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total']) @@ -1047,7 +1160,6 @@ class pos_order_line(osv.osv): prod = self.pool.get('product.product').browse(cr, uid, product, context=context) - taxes = prod.taxes_id price = price_unit * (1 - (discount or 0.0) / 100.0) taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False) @@ -1084,11 +1196,9 @@ class pos_order_line(osv.osv): }) return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context) -pos_order_line() - class pos_category(osv.osv): _name = 'pos.category' - _description = "PoS Category" + _description = "Point of Sale Category" _order = "sequence, name" def _check_recursion(self, cr, uid, ids, context=None): level = 100 @@ -1120,50 +1230,125 @@ class pos_category(osv.osv): res = self.name_get(cr, uid, ids, context=context) return dict(res) + def _get_image(self, cr, uid, ids, name, args, context=None): + result = dict.fromkeys(ids, False) + for obj in self.browse(cr, uid, ids, context=context): + result[obj.id] = tools.image_get_resized_images(obj.image) + return result + + def _set_image(self, cr, uid, id, name, value, args, context=None): + return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context) + _columns = { 'name': fields.char('Name', size=64, required=True, translate=True), 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'), 'parent_id': fields.many2one('pos.category','Parent Category', select=True), 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'), 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."), - 'to_weight' : fields.boolean('To Weight'), + + # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail + # for at least one category, then we display a default image on the other, so that the buttons have consistent styling. + # In this case, the default image is set by the js code. + # NOTE2: image: all image fields are base64 encoded and PIL-supported + 'image': fields.binary("Image", + help="This field holds the image used as image for the cateogry, limited to 1024x1024px."), + 'image_medium': fields.function(_get_image, fnct_inv=_set_image, + string="Medium-sized image", type="binary", multi="_get_image", + store={ + 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), + }, + help="Medium-sized image of the category. It is automatically "\ + "resized as a 128x128px image, with aspect ratio preserved. "\ + "Use this field in form views or some kanban views."), + 'image_small': fields.function(_get_image, fnct_inv=_set_image, + string="Smal-sized image", type="binary", multi="_get_image", + store={ + 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), + }, + help="Small-sized image of the category. It is automatically "\ + "resized as a 64x64px image, with aspect ratio preserved. "\ + "Use this field anywhere a small image is required."), } - _defaults = { - 'to_weight' : False, - } -pos_category() - import io, StringIO +class ean_wizard(osv.osv_memory): + _name = 'pos.ean_wizard' + _columns = { + 'ean13_pattern': fields.char('Reference', size=32, required=True, translate=True), + } + def sanitize_ean13(self, cr, uid, ids, context): + for r in self.browse(cr,uid,ids): + ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern) + m = context.get('active_model') + m_id = context.get('active_id') + self.pool.get(m).write(cr,uid,[m_id],{'ean13':ean13}) + return { 'type' : 'ir.actions.act_window_close' } + class product_product(osv.osv): _inherit = 'product.product' - def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None): - result = {} - for obj in self.browse(cr, uid, ids, context=context): - if not obj.product_image: - result[obj.id] = False - continue - image_stream = io.BytesIO(obj.product_image.decode('base64')) - img = Image.open(image_stream) - img.thumbnail((120, 100), Image.ANTIALIAS) - img_stream = StringIO.StringIO() - img.save(img_stream, "JPEG") - result[obj.id] = img_stream.getvalue().encode('base64') - return result + + #def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None): + # result = {} + # for obj in self.browse(cr, uid, ids, context=context): + # if not obj.product_image: + # result[obj.id] = False + # continue + + # image_stream = io.BytesIO(obj.product_image.decode('base64')) + # img = Image.open(image_stream) + # img.thumbnail((120, 100), Image.ANTIALIAS) + # img_stream = StringIO.StringIO() + # img.save(img_stream, "JPEG") + # result[obj.id] = img_stream.getvalue().encode('base64') + # return result _columns = { - 'income_pdt': fields.boolean('PoS Cash Input', help="This is a product you can use to put cash into a statement for the point of sale backend."), - 'expense_pdt': fields.boolean('PoS Cash Output', help="This is a product you can use to take cash from a statement for the point of sale backend, exemple: money lost, transfer to bank, etc."), - 'pos_categ_id': fields.many2one('pos.category','PoS Category', - help="If you want to sell this product through the point of sale, select the category it belongs to."), - 'product_image_small': fields.function(_get_small_image, string='Small Image', type="binary", - store = { - 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['product_image'], 10), - }) + 'income_pdt': fields.boolean('Point of Sale Cash In', help="Check if, this is a product you can use to put cash into a statement for the point of sale backend."), + 'expense_pdt': fields.boolean('Point of Sale Cash Out', help="Check if, this is a product you can use to take cash from a statement for the point of sale backend, example: money lost, transfer to bank, etc."), + 'available_in_pos': fields.boolean('Available in the Point of Sale', help='Check if you want this product to appear in the Point of Sale'), + 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', + help="The Point of Sale Category this products belongs to. Those categories are used to group similar products and are specific to the Point of Sale."), + 'to_weight' : fields.boolean('To Weight', help="Check if the product should be weighted (mainly used with self check-out interface)."), + } + + def _default_pos_categ_id(self, cr, uid, context=None): + proxy = self.pool.get('ir.model.data') + + try: + category_id = proxy.get_object_reference(cr, uid, 'point_of_sale', 'categ_others')[1] + except ValueError: + values = { + 'name' : 'Others', + } + category_id = self.pool.get('pos.category').create(cr, uid, values, context=context) + values = { + 'name' : 'categ_others', + 'model' : 'pos.category', + 'module' : 'point_of_sale', + 'res_id' : category_id, + } + proxy.create(cr, uid, values, context=context) + + return category_id + + _defaults = { + 'to_weight' : False, + 'available_in_pos': True, + 'pos_categ_id' : _default_pos_categ_id, } -product_product() + def edit_ean(self, cr, uid, ids, context): + return { + 'name': _("Assign a Custom EAN"), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'pos.ean_wizard', + 'target' : 'new', + 'view_id': False, + 'context':context, + } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: