From fcdd43010ac060b29901d7d8e1957c3545d7664e Mon Sep 17 00:00:00 2001 From: Darshan Kalola Date: Thu, 24 Sep 2015 14:43:25 +0530 Subject: [PATCH] [REF] point_of_sale: split files per model --- addons/point_of_sale/__openerp__.py | 39 +- addons/point_of_sale/models/__init__.py | 8 +- .../models/account_bank_statement.py | 15 +- .../point_of_sale/models/account_journal.py | 27 + addons/point_of_sale/models/barcode_rule.py | 20 + addons/point_of_sale/models/pos_category.py | 71 ++ addons/point_of_sale/models/pos_config.py | 324 ++++++ .../models/{point_of_sale.py => pos_order.py} | 750 +------------ addons/point_of_sale/models/pos_session.py | 307 ++++++ addons/point_of_sale/models/product.py | 27 + addons/point_of_sale/models/res_partner.py | 20 +- .../views/account_invoice_view.xml | 13 + .../views/account_journal_view.xml | 29 + .../views/point_of_sale_report.xml | 218 ++++ .../views/point_of_sale_view.xml | 984 ------------------ .../point_of_sale/views/pos_category_view.xml | 59 ++ .../point_of_sale/views/pos_config_view.xml | 147 +++ .../pos_order_report_view.xml | 0 addons/point_of_sale/views/pos_order_view.xml | 279 +++++ .../point_of_sale/views/pos_session_view.xml | 179 ++++ addons/point_of_sale/views/product_view.xml | 59 ++ addons/point_of_sale/wizard/pos_payment.py | 19 +- 22 files changed, 1818 insertions(+), 1776 deletions(-) create mode 100644 addons/point_of_sale/models/account_journal.py create mode 100644 addons/point_of_sale/models/barcode_rule.py create mode 100644 addons/point_of_sale/models/pos_category.py create mode 100644 addons/point_of_sale/models/pos_config.py rename addons/point_of_sale/models/{point_of_sale.py => pos_order.py} (53%) create mode 100644 addons/point_of_sale/models/pos_session.py create mode 100644 addons/point_of_sale/models/product.py create mode 100644 addons/point_of_sale/views/account_invoice_view.xml create mode 100644 addons/point_of_sale/views/account_journal_view.xml create mode 100644 addons/point_of_sale/views/pos_category_view.xml create mode 100644 addons/point_of_sale/views/pos_config_view.xml rename addons/point_of_sale/{report => views}/pos_order_report_view.xml (100%) create mode 100644 addons/point_of_sale/views/pos_order_view.xml create mode 100644 addons/point_of_sale/views/pos_session_view.xml create mode 100644 addons/point_of_sale/views/product_view.xml diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 6d34f8a5cc1..f3c455b9023 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -30,28 +30,35 @@ Main Features """, 'depends': ['sale_stock', 'barcodes'], 'data': [ - 'data/default_barcode_patterns.xml', 'security/point_of_sale_security.xml', 'security/ir.model.access.csv', + 'data/default_barcode_patterns.xml', 'wizard/pos_box.xml', 'wizard/pos_confirm.xml', 'wizard/pos_details.xml', 'wizard/pos_discount.xml', 'wizard/pos_open_statement.xml', 'wizard/pos_payment.xml', - 'views/templates.xml', - 'point_of_sale.xml', - 'point_of_sale_report.xml', - 'point_of_sale_view.xml', - 'point_of_sale_sequence.xml', - 'point_of_sale_data.xml', - 'report/pos_order_report_view.xml', - 'point_of_sale_workflow.xml', - 'account_statement_view.xml', - 'account_statement_report.xml', - 'res_users_view.xml', - 'res_partner_view.xml', - 'res_config_view.xml', + 'views/pos_templates.xml', + 'views/point_of_sale_template.xml', + 'views/point_of_sale_report.xml', + 'views/point_of_sale_view.xml', + 'views/pos_order_view.xml', + 'views/product_view.xml', + 'views/pos_category_view.xml', + 'views/account_journal_view.xml', + 'views/pos_config_view.xml', + 'views/pos_session_view.xml', + 'views/account_invoice_view.xml', + 'views/point_of_sale_sequence.xml', + 'data/point_of_sale_data.xml', + 'data/point_of_sale_workflow.xml', + 'views/pos_order_report_view.xml', + 'views/account_statement_view.xml', + 'views/account_statement_report.xml', + 'views/res_users_view.xml', + 'views/res_partner_view.xml', + 'views/res_config_view.xml', 'views/report_statement.xml', 'views/report_usersproduct.xml', 'views/report_saleslines.xml', @@ -60,10 +67,10 @@ Main Features 'views/report_sessionsummary.xml', 'views/report_userlabel.xml', 'views/point_of_sale.xml', - 'point_of_sale_dashboard.xml', + 'views/point_of_sale_dashboard.xml', ], 'demo': [ - 'point_of_sale_demo.xml', + 'data/point_of_sale_demo.xml', ], 'test': [ '../account/test/account_minimal_test.xml', diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py index d529cbf25e0..7170dbcf350 100644 --- a/addons/point_of_sale/models/__init__.py +++ b/addons/point_of_sale/models/__init__.py @@ -2,7 +2,13 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import account_bank_statement -import point_of_sale +import account_journal +import barcode_rule +import pos_category +import pos_config +import pos_order +import pos_session +import product import res_config import res_partner import res_users diff --git a/addons/point_of_sale/models/account_bank_statement.py b/addons/point_of_sale/models/account_bank_statement.py index 5faa31b3007..098b750beba 100644 --- a/addons/point_of_sale/models/account_bank_statement.py +++ b/addons/point_of_sale/models/account_bank_statement.py @@ -5,14 +5,6 @@ from openerp.osv import fields, osv -class account_journal(osv.osv): - _inherit = 'account.journal' - _columns = { - 'journal_user': fields.boolean('Active in Point of Sale', help="Check this box if this journal define a payment method that can be used in a point of sale."), - - 'amount_authorized_diff' : fields.float('Amount Authorized Difference', help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at the closing of his session saying that he needs to contact his manager."), - } - class account_bank_statement(osv.osv): _inherit = 'account.bank.statement' @@ -20,3 +12,10 @@ class account_bank_statement(osv.osv): 'pos_session_id' : fields.many2one('pos.session', string="Session", copy=False), 'account_id': fields.related('journal_id', 'default_debit_account_id', type='many2one', relation='account.account', readonly=True), } + + +class account_bank_statement_line(osv.osv): + _inherit = 'account.bank.statement.line' + _columns= { + 'pos_statement_id': fields.many2one('pos.order', string="POS statement", ondelete='cascade'), + } diff --git a/addons/point_of_sale/models/account_journal.py b/addons/point_of_sale/models/account_journal.py new file mode 100644 index 00000000000..f9310410467 --- /dev/null +++ b/addons/point_of_sale/models/account_journal.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# Copyright (C) 2004-2008 PC Solutions (). All Rights Reserved +from openerp.osv import fields, osv + +class account_journal(osv.osv): + _inherit = 'account.journal' + _columns = { + 'journal_user': fields.boolean('Active in Point of Sale', help="Check this box if this journal define a payment method that can be used in a point of sale."), + + 'amount_authorized_diff' : fields.float('Amount Authorized Difference', help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at the closing of his session saying that he needs to contact his manager."), + } + + def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): + if not context: + context = {} + session_id = context.get('pos_session_id', False) or False + + if session_id: + session = self.pool.get('pos.session').browse(cr, uid, session_id, context=context) + + if session: + journal_ids = [journal.id for journal in session.config_id.journal_ids] + args += [('id', 'in', journal_ids)] + + return super(account_journal, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count) diff --git a/addons/point_of_sale/models/barcode_rule.py b/addons/point_of_sale/models/barcode_rule.py new file mode 100644 index 00000000000..f1f5e2abbba --- /dev/null +++ b/addons/point_of_sale/models/barcode_rule.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import sets +from openerp import models +from openerp.tools.translate import _ + + +class barcode_rule(models.Model): + _inherit = 'barcode.rule' + + def _get_type_selection(self): + types = sets.Set(super(barcode_rule,self)._get_type_selection()) + types.update([ + ('weight', _('Weighted Product')), + ('price', _('Priced Product')), + ('discount', _('Discounted Product')), + ('client', _('Client')), + ('cashier', _('Cashier')) + ]) + return list(types) diff --git a/addons/point_of_sale/models/pos_category.py b/addons/point_of_sale/models/pos_category.py new file mode 100644 index 00000000000..8c9d2ba5253 --- /dev/null +++ b/addons/point_of_sale/models/pos_category.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import tools +from openerp.osv import fields, osv + + +class pos_category(osv.osv): + _name = "pos.category" + _description = "Public Category" + _order = "sequence, name" + + _constraints = [ + (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id']) + ] + + def name_get(self, cr, uid, ids, context=None): + res = [] + for cat in self.browse(cr, uid, ids, context=context): + names = [cat.name] + pcat = cat.parent_id + while pcat: + names.append(pcat.name) + pcat = pcat.parent_id + res.append((cat.id, ' / '.join(reversed(names)))) + return res + + def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): + res = self.name_get(cr, uid, ids, context=context) + return dict(res) + + _columns = { + 'name': fields.char('Name', 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."), + + } + + # 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. + image = openerp.fields.Binary("Image", attachment=True, + help="This field holds the image used as image for the cateogry, limited to 1024x1024px.") + image_medium = openerp.fields.Binary("Medium-sized image", + compute='_compute_images', inverse='_inverse_image_medium', store=True, attachment=True, + 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 = openerp.fields.Binary("Small-sized image", + compute='_compute_images', inverse='_inverse_image_small', store=True, attachment=True, + 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.") + + @openerp.api.depends('image') + def _compute_images(self): + for rec in self: + rec.image_medium = tools.image_resize_image_medium(rec.image) + rec.image_small = tools.image_resize_image_small(rec.image) + + def _inverse_image_medium(self): + for rec in self: + rec.image = tools.image_resize_image_big(rec.image_medium) + + def _inverse_image_small(self): + for rec in self: + rec.image = tools.image_resize_image_big(rec.image_small) diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py new file mode 100644 index 00000000000..dfa363d1745 --- /dev/null +++ b/addons/point_of_sale/models/pos_config.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import uuid + +from openerp import SUPERUSER_ID +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +class pos_config(osv.osv): + _name = 'pos.config' + + POS_CONFIG_STATE = [ + ('active', 'Active'), + ('inactive', 'Inactive'), + ('deprecated', 'Deprecated') + ] + + def _get_currency(self, cr, uid, ids, fieldnames, args, context=None): + result = dict.fromkeys(ids, False) + for pos_config in self.browse(cr, uid, ids, context=context): + if pos_config.journal_id: + currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id + else: + currency_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id + result[pos_config.id] = currency_id + return result + + def _get_current_session(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + + for record in self.browse(cr, uid, ids, context=context): + session_id = record.session_ids.filtered(lambda r: r.user_id.id == uid and not r.state == 'closed') + result[record.id] = { + 'current_session_id': session_id, + 'current_session_state': session_id.state, + } + return result + + def _get_last_session(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + + for record in self.browse(cr, uid, ids, context=context): + session_ids = self.pool['pos.session'].search_read( + cr, uid, + [('config_id', '=', record.id), ('state', '=', 'closed')], + ['cash_register_balance_end_real', 'stop_at'], + order="stop_at desc", limit=1, context=context) + if session_ids: + result[record.id] = { + 'last_session_closing_cash': session_ids[0]['cash_register_balance_end_real'], + 'last_session_closing_date': session_ids[0]['stop_at'], + } + else: + result[record.id] = { + 'last_session_closing_cash': 0, + 'last_session_closing_date': None, + } + return result + + def _get_current_session_user(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + + for record in self.browse(cr, uid, ids, context=context): + result[record.id] = record.session_ids.filtered(lambda r: r.state == 'opened').user_id.name + return result + + _columns = { + 'name' : fields.char('Point of Sale Name', 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 ), ('type', 'in', ['bank', 'cash'])]",), + 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'), + 'stock_location_id': fields.many2one('stock.location', 'Stock Location', domain=[('usage', '=', 'internal')], required=True), + 'journal_id' : fields.many2one('account.journal', 'Sale Journal', + domain=[('type', '=', 'sale')], + help="Accounting journal used to post sales entries."), + 'currency_id' : fields.function(_get_currency, type="many2one", string="Currency", relation="res.currency"), + 'iface_cashdrawer' : fields.boolean('Cashdrawer', help="Automatically open the cashdrawer"), + 'iface_payment_terminal' : fields.boolean('Payment Terminal', help="Enables Payment Terminal integration"), + 'iface_electronic_scale' : fields.boolean('Electronic Scale', help="Enables Electronic Scale integration"), + 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard', help="Enables an integrated Virtual Keyboard"), + 'iface_print_via_proxy' : fields.boolean('Print via Proxy', help="Bypass browser printing and prints via the hardware proxy"), + 'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"), + 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), + 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), + # TODO master: remove the `iface_fullscreen` field. This is no longer used. + 'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'), + 'iface_print_auto': fields.boolean('Automatic Receipt Printing', help='The receipt will automatically be printed at the end of each order'), + 'iface_print_skip_screen': fields.boolean('Skip Receipt Screen', help='The receipt screen will be skipped if the receipt can be printed automatically.'), + 'iface_precompute_cash': fields.boolean('Prefill Cash Payment', help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount'), + 'iface_tax_included': fields.boolean('Include Taxes in Prices', help='The displayed prices will always include all taxes, even if the taxes have been setup differently'), + 'iface_start_categ_id': fields.many2one('pos.category','Start Category', help='The point of sale will display this product category by default. If no category is specified, all available products will be shown'), + 'iface_display_categ_images': fields.boolean('Display Category Pictures', help="The product categories will be displayed with pictures."), + 'cash_control': fields.boolean('Cash Control', help="Check the amount of the cashbox at opening and closing."), + 'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"), + 'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"), + 'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45), + + 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True, copy=False), + 'uuid' : fields.char('uuid', readonly=True, help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data'), + 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, + help="This sequence is automatically created by Odoo but you can change it "\ + "to customize the reference numbers of your orders.", copy=False), + 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'), + 'current_session_id': fields.function(_get_current_session, multi="session", type="many2one", relation="pos.session", string="Current Session"), + 'current_session_state': fields.function(_get_current_session, multi="session", type='char'), + 'last_session_closing_cash': fields.function(_get_last_session, multi="last_session", type='float'), + 'last_session_closing_date': fields.function(_get_last_session, multi="last_session", type='date'), + 'pos_session_username': fields.function(_get_current_session_user, type='char'), + 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), + 'pricelist_id': fields.many2one('product.pricelist','Pricelist', required=True), + 'company_id': fields.many2one('res.company', 'Company', required=True), + 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcodes', help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers', required=True), + 'group_pos_manager_id': fields.many2one('res.groups','Point of Sale Manager Group', help='This field is there to pass the id of the pos manager group to the point of sale client'), + 'group_pos_user_id': fields.many2one('res.groups','Point of Sale User Group', help='This field is there to pass the id of the pos user group to the point of sale client'), + 'tip_product_id': fields.many2one('product.product','Tip Product', help="The product used to encode the customer tip. Leave empty if you do not accept tips."), + 'fiscal_position_ids': fields.many2many('account.fiscal.position', string='Fiscal Positions') + } + + def _check_company_location(self, cr, uid, ids, context=None): + for config in self.browse(cr, uid, ids, context=context): + if config.stock_location_id.company_id and config.stock_location_id.company_id.id != config.company_id.id: + return False + return True + + def _check_company_journal(self, cr, uid, ids, context=None): + for config in self.browse(cr, uid, ids, context=context): + if config.journal_id and config.journal_id.company_id.id != config.company_id.id: + return False + return True + + def _check_company_payment(self, cr, uid, ids, context=None): + for config in self.browse(cr, uid, ids, context=context): + journal_ids = [j.id for j in config.journal_ids] + if self.pool['account.journal'].search(cr, uid, [ + ('id', 'in', journal_ids), + ('company_id', '!=', config.company_id.id) + ], count=True, context=context): + return False + return True + + _constraints = [ + (_check_company_location, "The company of the stock location is different than the one of point of sale", ['company_id', 'stock_location_id']), + (_check_company_journal, "The company of the sale journal is different than the one of point of sale", ['company_id', 'journal_id']), + (_check_company_payment, "The company of a payment method is different than the one of point of sale", ['company_id', 'journal_ids']), + ] + + 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): + 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 + + def _default_sale_journal(self, cr, uid, context=None): + company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id + res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', company_id)], limit=1, context=context) + return res and res[0] or False + + def _default_pricelist(self, cr, uid, context=None): + res = self.pool.get('product.pricelist').search(cr, uid, [], limit=1, context=context) + return res and res[0] or False + + def _get_default_location(self, cr, uid, context=None): + wh_obj = self.pool.get('stock.warehouse') + user = self.pool.get('res.users').browse(cr, uid, uid, context) + res = wh_obj.search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context) + if res and res[0]: + return wh_obj.browse(cr, uid, res[0], context=context).lot_stock_id.id + return False + + def _get_default_company(self, cr, uid, context=None): + company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) + return company_id + + def _get_default_nomenclature(self, cr, uid, context=None): + nom_obj = self.pool.get('barcode.nomenclature') + res = nom_obj.search(cr, uid, [], limit=1, context=context) + return res and res[0] or False + + def _get_group_pos_manager(self, cr, uid, context=None): + group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_manager') + if group: + return group[1] + else: + return False + + def _get_group_pos_user(self, cr, uid, context=None): + group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_user') + if group: + return group[1] + else: + return False + + _defaults = { + 'uuid' : lambda self, cr, uid, context={}: str(uuid.uuid4()), + 'state' : POS_CONFIG_STATE[0][0], + 'journal_id': _default_sale_journal, + 'group_by' : True, + 'pricelist_id': _default_pricelist, + 'iface_invoicing': True, + 'iface_print_auto': False, + 'iface_print_skip_screen': True, + 'stock_location_id': _get_default_location, + 'company_id': _get_default_company, + 'barcode_nomenclature_id': _get_default_nomenclature, + 'group_pos_manager_id': _get_group_pos_manager, + 'group_pos_user_id': _get_group_pos_user, + } + + def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None): + p_type_obj = self.pool.get("stock.picking.type") + p_type = p_type_obj.browse(cr, uid, picking_type_id, context=context) + if p_type.default_location_src_id and p_type.default_location_src_id.usage == 'internal' and p_type.default_location_dest_id and p_type.default_location_dest_id.usage == 'customer': + return {'value': {'stock_location_id': p_type.default_location_src_id.id}} + return False + + def onchange_iface_print_via_proxy(self, cr, uid, ids, print_via_proxy, context=None): + return {'value': {'iface_print_auto': print_via_proxy}} + + def set_active(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'active'}, context=context) + + def set_inactive(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context) + + def set_deprecate(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context) + + def create(self, cr, uid, values, context=None): + ir_sequence = self.pool.get('ir.sequence') + # force sequence_id field to new pos.order sequence + values['sequence_id'] = ir_sequence.create(cr, SUPERUSER_ID, { + 'name': 'POS Order %s' % values['name'], + 'padding': 4, + 'prefix': "%s/" % values['name'], + 'code': "pos.order", + 'company_id': values.get('company_id', False), + }, context=context) + + # TODO master: add field sequence_line_id on model + # this make sure we always have one available per company + ir_sequence.create(cr, SUPERUSER_ID, { + 'name': 'POS order line %s' % values['name'], + 'padding': 4, + 'prefix': "%s/" % values['name'], + 'code': "pos.order.line", + 'company_id': values.get('company_id', False), + }, context=context) + + return super(pos_config, self).create(cr, uid, 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) + + # Methods to open the POS + + def open_ui(self, cr, uid, ids, context=None): + assert len(ids) == 1, "you can open only one session at a time" + + record = self.browse(cr, uid, ids[0], context=context) + context = dict(context or {}) + context['active_id'] = record.current_session_id.id + return { + 'type': 'ir.actions.act_url', + 'url': '/pos/web/', + 'target': 'self', + } + + def open_existing_session_cb_close(self, cr, uid, ids, context=None): + assert len(ids) == 1, "you can open only one session at a time" + + record = self.browse(cr, uid, ids[0], context=context) + record.current_session_id.signal_workflow('cashbox_control') + return self.open_session_cb(cr, uid, ids, context) + + def open_session_cb(self, cr, uid, ids, context=None): + assert len(ids) == 1, "you can open only one session at a time" + + proxy = self.pool.get('pos.session') + record = self.browse(cr, uid, ids[0], context=context) + current_session_id = record.current_session_id + if not current_session_id: + values = { + 'user_id': uid, + 'config_id': record.id, + } + session_id = proxy.create(cr, uid, values, context=context) + self.write(cr, SUPERUSER_ID, record.id, {'current_session_id': session_id}, context=context) + if record.current_session_id.state == 'opened': + return self.open_ui(cr, uid, ids, context=context) + return self._open_session(session_id) + return self._open_session(current_session_id.id) + + def open_existing_session_cb(self, cr, uid, ids, context=None): + assert len(ids) == 1, "you can open only one session at a time" + + record = self.browse(cr, uid, ids[0], context=context) + return self._open_session(record.current_session_id.id) + + def _open_session(self, session_id): + return { + 'name': _('Session'), + 'view_type': 'form', + 'view_mode': 'form,tree', + 'res_model': 'pos.session', + 'res_id': session_id, + 'view_id': False, + 'type': 'ir.actions.act_window', + } diff --git a/addons/point_of_sale/models/point_of_sale.py b/addons/point_of_sale/models/pos_order.py similarity index 53% rename from addons/point_of_sale/models/point_of_sale.py rename to addons/point_of_sale/models/pos_order.py index b09fcc8a722..65e4754908e 100644 --- a/addons/point_of_sale/models/point_of_sale.py +++ b/addons/point_of_sale/models/pos_order.py @@ -2,16 +2,15 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging + import time from datetime import datetime -import uuid -import sets from functools import partial import openerp import openerp.addons.decimal_precision as dp -from openerp import tools, models, SUPERUSER_ID +from openerp import tools, SUPERUSER_ID from openerp.osv import fields, osv from openerp.tools import float_is_zero from openerp.tools.translate import _ @@ -20,623 +19,6 @@ from openerp.exceptions import UserError _logger = logging.getLogger(__name__) -class pos_config(osv.osv): - _name = 'pos.config' - - POS_CONFIG_STATE = [ - ('active', 'Active'), - ('inactive', 'Inactive'), - ('deprecated', 'Deprecated') - ] - - def _get_currency(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, False) - for pos_config in self.browse(cr, uid, ids, context=context): - if pos_config.journal_id: - currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id - else: - currency_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id - result[pos_config.id] = currency_id - return result - - def _get_current_session(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - session_id = record.session_ids.filtered(lambda r: r.user_id.id == uid and not r.state == 'closed') - result[record.id] = { - 'current_session_id': session_id, - 'current_session_state': session_id.state, - } - return result - - def _get_last_session(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - session_ids = self.pool['pos.session'].search_read( - cr, uid, - [('config_id', '=', record.id), ('state', '=', 'closed')], - ['cash_register_balance_end_real', 'stop_at'], - order="stop_at desc", limit=1, context=context) - if session_ids: - result[record.id] = { - 'last_session_closing_cash': session_ids[0]['cash_register_balance_end_real'], - 'last_session_closing_date': session_ids[0]['stop_at'], - } - else: - result[record.id] = { - 'last_session_closing_cash': 0, - 'last_session_closing_date': None, - } - return result - - def _get_current_session_user(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - result[record.id] = record.session_ids.filtered(lambda r: r.state == 'opened').user_id.name - return result - - _columns = { - 'name' : fields.char('Point of Sale Name', 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 ), ('type', 'in', ['bank', 'cash'])]",), - 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'), - 'stock_location_id': fields.many2one('stock.location', 'Stock Location', domain=[('usage', '=', 'internal')], required=True), - 'journal_id' : fields.many2one('account.journal', 'Sale Journal', - domain=[('type', '=', 'sale')], - help="Accounting journal used to post sales entries."), - 'currency_id' : fields.function(_get_currency, type="many2one", string="Currency", relation="res.currency"), - 'iface_cashdrawer' : fields.boolean('Cashdrawer', help="Automatically open the cashdrawer"), - 'iface_payment_terminal' : fields.boolean('Payment Terminal', help="Enables Payment Terminal integration"), - 'iface_electronic_scale' : fields.boolean('Electronic Scale', help="Enables Electronic Scale integration"), - 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard', help="Enables an integrated Virtual Keyboard"), - 'iface_print_via_proxy' : fields.boolean('Print via Proxy', help="Bypass browser printing and prints via the hardware proxy"), - 'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"), - 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), - 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), - # TODO master: remove the `iface_fullscreen` field. This is no longer used. - 'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'), - 'iface_print_auto': fields.boolean('Automatic Receipt Printing', help='The receipt will automatically be printed at the end of each order'), - 'iface_print_skip_screen': fields.boolean('Skip Receipt Screen', help='The receipt screen will be skipped if the receipt can be printed automatically.'), - 'iface_precompute_cash': fields.boolean('Prefill Cash Payment', help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount'), - 'iface_tax_included': fields.boolean('Include Taxes in Prices', help='The displayed prices will always include all taxes, even if the taxes have been setup differently'), - 'iface_start_categ_id': fields.many2one('pos.category','Start Category', help='The point of sale will display this product category by default. If no category is specified, all available products will be shown'), - 'iface_display_categ_images': fields.boolean('Display Category Pictures', help="The product categories will be displayed with pictures."), - 'cash_control': fields.boolean('Cash Control', help="Check the amount of the cashbox at opening and closing."), - 'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"), - 'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"), - 'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45), - - 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True, copy=False), - 'uuid' : fields.char('uuid', readonly=True, help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data'), - 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, - help="This sequence is automatically created by Odoo but you can change it "\ - "to customize the reference numbers of your orders.", copy=False), - 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'), - 'current_session_id': fields.function(_get_current_session, multi="session", type="many2one", relation="pos.session", string="Current Session"), - 'current_session_state': fields.function(_get_current_session, multi="session", type='char'), - 'last_session_closing_cash': fields.function(_get_last_session, multi="last_session", type='float'), - 'last_session_closing_date': fields.function(_get_last_session, multi="last_session", type='date'), - 'pos_session_username': fields.function(_get_current_session_user, type='char'), - 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), - 'pricelist_id': fields.many2one('product.pricelist','Pricelist', required=True), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcodes', help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers', required=True), - 'group_pos_manager_id': fields.many2one('res.groups','Point of Sale Manager Group', help='This field is there to pass the id of the pos manager group to the point of sale client'), - 'group_pos_user_id': fields.many2one('res.groups','Point of Sale User Group', help='This field is there to pass the id of the pos user group to the point of sale client'), - 'tip_product_id': fields.many2one('product.product','Tip Product', help="The product used to encode the customer tip. Leave empty if you do not accept tips."), - 'fiscal_position_ids': fields.many2many('account.fiscal.position', string='Fiscal Positions') - } - - def _check_company_location(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - if config.stock_location_id.company_id and config.stock_location_id.company_id.id != config.company_id.id: - return False - return True - - def _check_company_journal(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - if config.journal_id and config.journal_id.company_id.id != config.company_id.id: - return False - return True - - def _check_company_payment(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - journal_ids = [j.id for j in config.journal_ids] - if self.pool['account.journal'].search(cr, uid, [ - ('id', 'in', journal_ids), - ('company_id', '!=', config.company_id.id) - ], count=True, context=context): - return False - return True - - _constraints = [ - (_check_company_location, "The company of the stock location is different than the one of point of sale", ['company_id', 'stock_location_id']), - (_check_company_journal, "The company of the sale journal is different than the one of point of sale", ['company_id', 'journal_id']), - (_check_company_payment, "The company of a payment method is different than the one of point of sale", ['company_id', 'journal_ids']), - ] - - 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): - 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 - - def _default_sale_journal(self, cr, uid, context=None): - company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', company_id)], limit=1, context=context) - return res and res[0] or False - - def _default_pricelist(self, cr, uid, context=None): - res = self.pool.get('product.pricelist').search(cr, uid, [], limit=1, context=context) - return res and res[0] or False - - def _get_default_location(self, cr, uid, context=None): - wh_obj = self.pool.get('stock.warehouse') - user = self.pool.get('res.users').browse(cr, uid, uid, context) - res = wh_obj.search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context) - if res and res[0]: - return wh_obj.browse(cr, uid, res[0], context=context).lot_stock_id.id - return False - - def _get_default_company(self, cr, uid, context=None): - company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) - return company_id - - def _get_default_nomenclature(self, cr, uid, context=None): - nom_obj = self.pool.get('barcode.nomenclature') - res = nom_obj.search(cr, uid, [], limit=1, context=context) - return res and res[0] or False - - def _get_group_pos_manager(self, cr, uid, context=None): - group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_manager') - if group: - return group[1] - else: - return False - - def _get_group_pos_user(self, cr, uid, context=None): - group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_user') - if group: - return group[1] - else: - return False - - _defaults = { - 'uuid' : lambda self, cr, uid, context={}: str(uuid.uuid4()), - 'state' : POS_CONFIG_STATE[0][0], - 'journal_id': _default_sale_journal, - 'group_by' : True, - 'pricelist_id': _default_pricelist, - 'iface_invoicing': True, - 'iface_print_auto': False, - 'iface_print_skip_screen': True, - 'stock_location_id': _get_default_location, - 'company_id': _get_default_company, - 'barcode_nomenclature_id': _get_default_nomenclature, - 'group_pos_manager_id': _get_group_pos_manager, - 'group_pos_user_id': _get_group_pos_user, - } - - def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None): - p_type_obj = self.pool.get("stock.picking.type") - p_type = p_type_obj.browse(cr, uid, picking_type_id, context=context) - if p_type.default_location_src_id and p_type.default_location_src_id.usage == 'internal' and p_type.default_location_dest_id and p_type.default_location_dest_id.usage == 'customer': - return {'value': {'stock_location_id': p_type.default_location_src_id.id}} - return False - - def onchange_iface_print_via_proxy(self, cr, uid, ids, print_via_proxy, context=None): - return {'value': {'iface_print_auto': print_via_proxy}} - - def set_active(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'active'}, context=context) - - def set_inactive(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context) - - def set_deprecate(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context) - - def create(self, cr, uid, values, context=None): - ir_sequence = self.pool.get('ir.sequence') - # force sequence_id field to new pos.order sequence - values['sequence_id'] = ir_sequence.create(cr, SUPERUSER_ID, { - 'name': 'POS Order %s' % values['name'], - 'padding': 4, - 'prefix': "%s/" % values['name'], - 'code': "pos.order", - 'company_id': values.get('company_id', False), - }, context=context) - - # TODO master: add field sequence_line_id on model - # this make sure we always have one available per company - ir_sequence.create(cr, SUPERUSER_ID, { - 'name': 'POS order line %s' % values['name'], - 'padding': 4, - 'prefix': "%s/" % values['name'], - 'code': "pos.order.line", - 'company_id': values.get('company_id', False), - }, context=context) - - return super(pos_config, self).create(cr, uid, 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) - - # Methods to open the POS - - def open_ui(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - record = self.browse(cr, uid, ids[0], context=context) - context = dict(context or {}) - context['active_id'] = record.current_session_id.id - return { - 'type': 'ir.actions.act_url', - 'url': '/pos/web/', - 'target': 'self', - } - - def open_existing_session_cb_close(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - record = self.browse(cr, uid, ids[0], context=context) - record.current_session_id.signal_workflow('cashbox_control') - return self.open_session_cb(cr, uid, ids, context) - - def open_session_cb(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - proxy = self.pool.get('pos.session') - record = self.browse(cr, uid, ids[0], context=context) - current_session_id = record.current_session_id - if not current_session_id: - values = { - 'user_id': uid, - 'config_id': record.id, - } - session_id = proxy.create(cr, uid, values, context=context) - self.write(cr, SUPERUSER_ID, record.id, {'current_session_id': session_id}, context=context) - if record.current_session_id.state == 'opened': - return self.open_ui(cr, uid, ids, context=context) - return self._open_session(session_id) - return self._open_session(current_session_id.id) - - def open_existing_session_cb(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - record = self.browse(cr, uid, ids[0], context=context) - return self._open_session(record.current_session_id.id) - - def _open_session(self, session_id): - return { - 'name': _('Session'), - 'view_type': 'form', - 'view_mode': 'form,tree', - 'res_model': 'pos.session', - 'res_id': session_id, - 'view_id': False, - 'type': 'ir.actions.act_window', - } - -class pos_session(osv.osv): - _name = 'pos.session' - _order = 'id desc' - - POS_SESSION_STATE = [ - ('opening_control', 'Opening Control'), # Signal open - ('opened', 'In Progress'), # Signal closing - ('closing_control', 'Closing Control'), # Signal close - ('closed', 'Closed & Posted'), - ] - - def _compute_cash_all(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - result[record.id] = { - 'cash_journal_id' : False, - 'cash_register_id' : False, - 'cash_control' : False, - } - - if record.config_id.cash_control: - for st in record.statement_ids: - if st.journal_id.type == 'cash': - result[record.id]['cash_control'] = True - result[record.id]['cash_journal_id'] = st.journal_id.id - result[record.id]['cash_register_id'] = st.id - - if not result[record.id]['cash_control']: - raise UserError(_("Cash control can only be applied to cash journals.")) - - return result - - _columns = { - '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')]", - ), - - 'name' : fields.char('Session ID', required=True, readonly=True), - 'user_id' : fields.many2one('res.users', 'Responsible', - required=True, - select=1, - readonly=True, - states={'opening_control' : [('readonly', False)]} - ), - 'currency_id' : fields.related('config_id', 'currency_id', type="many2one", relation='res.currency', string="Currency"), - 'start_at' : fields.datetime('Opening Date', readonly=True), - 'stop_at' : fields.datetime('Closing Date', readonly=True, copy=False), - - 'state' : fields.selection(POS_SESSION_STATE, 'Status', - required=True, readonly=True, - select=1, copy=False), - - 'sequence_number': fields.integer('Order Sequence Number', help='A sequence number that is incremented with each order'), - 'login_number': fields.integer('Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session'), - - '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), - - 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real', - type='float', - digits=0, - string="Ending Balance", - help="Total of closing cash control lines.", - readonly=True), - 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start', - type='float', - digits=0, - string="Starting Balance", - help="Total of opening cash control lines.", - readonly=True), - 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding', - string='Total Cash Transaction', - readonly=True, - help="Total of all paid sale orders"), - 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end', - type='float', - digits=0, - string="Theoretical Closing Balance", - help="Sum of opening balance and transactions.", - readonly=True), - 'cash_register_difference' : fields.related('cash_register_id', 'difference', - type='float', - string='Difference', - help="Difference between the theoretical closing balance and the real closing balance.", - readonly=True), - - 'journal_ids' : fields.related('config_id', 'journal_ids', - type='many2many', - readonly=True, - relation='account.journal', - string='Available Payment Methods'), - 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'), - - 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True), - } - - _defaults = { - 'name' : '/', - 'user_id' : lambda obj, cr, uid, context: uid, - 'state' : 'opening_control', - 'sequence_number': 1, - 'login_number': 0, - } - - _sql_constraints = [ - ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"), - ] - - 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', '=', session.user_id.id) - ] - count = self.search_count(cr, uid, domain, context=context) - if count>1: - return False - return True - - 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 - - _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']), - ] - - def create(self, cr, uid, values, context=None): - context = dict(context or {}) - config_id = values.get('config_id', False) or context.get('default_config_id', False) - if not config_id: - raise UserError(_("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.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, SUPERUSER_ID, [pos_config.id], {'journal_id': jid}, context=context) - else: - raise UserError(_("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) - - journal_proxy.write(cr, SUPERUSER_ID, cashids, {'journal_user': True}) - jobj.write(cr, SUPERUSER_ID, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) - - - pos_config = jobj.browse(cr, uid, config_id, context=context) - - statements = [(0, 0, { - 'journal_id': journal.id, - 'user_id': uid, - 'company_id': pos_config.company_id.id - }) for journal in pos_config.journal_ids] - - values.update({ - 'name': self.pool['ir.sequence'].next_by_code(cr, uid, 'pos.session', context=context), - 'statement_ids': statements, - 'config_id': config_id - }) - - # set the journal_id which should be used by - # account.bank.statement to set the opening balance of the - # newly created bank statement - if pos_config.cash_control: - for journal in pos_config.journal_ids: - if journal.type == 'cash': - context.update({'journal_id': journal.id}) - - return super(pos_session, self).create(cr, uid, values, context=context) - - def unlink(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.pool.get('account.bank.statement').unlink(cr, uid, obj.statement_ids.ids, context=context) - return super(pos_session, self).unlink(cr, uid, ids, context=context) - - def login(self, cr, uid, ids, context=None): - this_record = self.browse(cr, uid, ids[0], context=context) - this_record.write({ - 'login_number': this_record.login_number+1, - }) - - def wkf_action_open(self, cr, uid, ids, context=None): - # 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) - for st in record.statement_ids: - st.button_open() - - return True - - 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 - local_context = dict(context) - for record in self.browse(cr, uid, ids, context=context): - company_id = record.config_id.company_id.id - local_context.update({'force_company': company_id, 'company_id': company_id}) - for st in record.statement_ids: - 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 UserError(_("Your ending balance is too different from the theoretical 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.journal_id.type not in ['bank', 'cash']): - raise UserError(_("The type of the journal for your payment method should be bank or cash ")) - self.pool['account.bank.statement'].button_confirm_bank(cr, SUPERUSER_ID, [st.id], context=local_context) - self._confirm_orders(cr, uid, ids, context=local_context) - self.write(cr, uid, ids, {'state' : 'closed'}, context=local_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): - pos_order_obj = self.pool.get('pos.order') - for session in self.browse(cr, uid, ids, context=context): - company_id = session.config_id.journal_id.company_id.id - local_context = dict(context or {}, force_company=company_id) - order_ids = [order.id for order in session.order_ids if order.state == 'paid'] - - move_id = pos_order_obj._create_account_move(cr, uid, session.start_at, session.name, session.config_id.journal_id.id, company_id, context=context) - - pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=local_context) - - for order in session.order_ids: - if order.state == 'done': - continue - if order.state not in ('paid', 'invoiced'): - raise UserError(_("You cannot confirm all orders of this session, because they have not the 'paid' status")) - else: - pos_order_obj.signal_workflow(cr, uid, [order.id], 'done') - - return True - - def open_frontend_cb(self, cr, uid, ids, context=None): - if not context: - context = {} - if not ids: - return {} - for session in self.browse(cr, uid, ids, context=context): - if session.user_id.id != uid: - raise UserError(_("You cannot use the session of another users. This session is owned by %s. " - "Please first close this one to use this point of sale.") % session.user_id.name) - context.update({'active_id': ids[0]}) - return { - 'type' : 'ir.actions.act_url', - 'target': 'self', - 'url': '/pos/web/', - } class pos_order(osv.osv): _name = "pos.order" @@ -1377,12 +759,6 @@ class pos_order(osv.osv): self.create_account_move(cr, uid, ids, context=context) return True -class account_bank_statement_line(osv.osv): - _inherit = 'account.bank.statement.line' - _columns= { - 'pos_statement_id': fields.many2one('pos.order', string="POS statement", ondelete='cascade'), - } - class pos_order_line(osv.osv): _name = "pos.order.line" _description = "Lines of Point of Sale" @@ -1480,125 +856,3 @@ class pos_order_line(osv.osv): 'discount': lambda *a: 0.0, 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, } - -class pos_category(osv.osv): - _name = "pos.category" - _description = "Public Category" - _order = "sequence, name" - - _constraints = [ - (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id']) - ] - - def name_get(self, cr, uid, ids, context=None): - res = [] - for cat in self.browse(cr, uid, ids, context=context): - names = [cat.name] - pcat = cat.parent_id - while pcat: - names.append(pcat.name) - pcat = pcat.parent_id - res.append((cat.id, ' / '.join(reversed(names)))) - return res - - def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): - res = self.name_get(cr, uid, ids, context=context) - return dict(res) - - _columns = { - 'name': fields.char('Name', 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."), - - } - - # 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. - image = openerp.fields.Binary("Image", attachment=True, - help="This field holds the image used as image for the cateogry, limited to 1024x1024px.") - image_medium = openerp.fields.Binary("Medium-sized image", - compute='_compute_images', inverse='_inverse_image_medium', store=True, attachment=True, - 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 = openerp.fields.Binary("Small-sized image", - compute='_compute_images', inverse='_inverse_image_small', store=True, attachment=True, - 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.") - - @openerp.api.depends('image') - def _compute_images(self): - for rec in self: - rec.image_medium = tools.image_resize_image_medium(rec.image) - rec.image_small = tools.image_resize_image_small(rec.image) - - def _inverse_image_medium(self): - for rec in self: - rec.image = tools.image_resize_image_big(rec.image_medium) - - def _inverse_image_small(self): - for rec in self: - rec.image = tools.image_resize_image_big(rec.image_small) - -class product_template(osv.osv): - _inherit = 'product.template' - - _columns = { - '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'), - 'to_weight' : fields.boolean('To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration"), - 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', help="Those categories are used to group similar products for point of sale."), - } - - _defaults = { - 'to_weight' : False, - 'available_in_pos': True, - } - - def unlink(self, cr, uid, ids, context=None): - product_ctx = dict(context or {}, active_test=False) - if self.search_count(cr, uid, [('id', 'in', ids), ('available_in_pos', '=', True)], context=product_ctx): - if self.pool['pos.session'].search_count(cr, uid, [('state', '!=', 'closed')], context=context): - raise osv.except_osv(_('Error!'), - _('You cannot delete a product saleable in point of sale while a session is still opened.')) - return super(product_template, self).unlink(cr, uid, ids, context=context) - -class res_partner(osv.osv): - _inherit = 'res.partner' - - def create_from_ui(self, cr, uid, partner, context=None): - """ create or modify a partner from the point of sale ui. - partner contains the partner's fields. """ - - #image is a dataurl, get the data after the comma - if partner.get('image',False): - img = partner['image'].split(',')[1] - partner['image'] = img - - if partner.get('id',False): # Modifying existing partner - partner_id = partner['id'] - del partner['id'] - self.write(cr, uid, [partner_id], partner, context=context) - else: - partner_id = self.create(cr, uid, partner, context=context) - - return partner_id - -class barcode_rule(models.Model): - _inherit = 'barcode.rule' - - def _get_type_selection(self): - types = sets.Set(super(barcode_rule,self)._get_type_selection()) - types.update([ - ('weight', _('Weighted Product')), - ('price', _('Priced Product')), - ('discount', _('Discounted Product')), - ('client', _('Client')), - ('cashier', _('Cashier')) - ]) - return list(types) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py new file mode 100644 index 00000000000..b1f408afb68 --- /dev/null +++ b/addons/point_of_sale/models/pos_session.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import time +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +class pos_session(osv.osv): + _name = 'pos.session' + _order = 'id desc' + + POS_SESSION_STATE = [ + ('opening_control', 'Opening Control'), # Signal open + ('opened', 'In Progress'), # Signal closing + ('closing_control', 'Closing Control'), # Signal close + ('closed', 'Closed & Posted'), + ] + + def _compute_cash_all(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + + for record in self.browse(cr, uid, ids, context=context): + result[record.id] = { + 'cash_journal_id' : False, + 'cash_register_id' : False, + 'cash_control' : False, + } + + if record.config_id.cash_control: + for st in record.statement_ids: + if st.journal_id.type == 'cash': + result[record.id]['cash_control'] = True + result[record.id]['cash_journal_id'] = st.journal_id.id + result[record.id]['cash_register_id'] = st.id + + if not result[record.id]['cash_control']: + raise UserError(_("Cash control can only be applied to cash journals.")) + + return result + + _columns = { + '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')]", + ), + + 'name' : fields.char('Session ID', required=True, readonly=True), + 'user_id' : fields.many2one('res.users', 'Responsible', + required=True, + select=1, + readonly=True, + states={'opening_control' : [('readonly', False)]} + ), + 'currency_id' : fields.related('config_id', 'currency_id', type="many2one", relation='res.currency', string="Currency"), + 'start_at' : fields.datetime('Opening Date', readonly=True), + 'stop_at' : fields.datetime('Closing Date', readonly=True, copy=False), + + 'state' : fields.selection(POS_SESSION_STATE, 'Status', + required=True, readonly=True, + select=1, copy=False), + + 'sequence_number': fields.integer('Order Sequence Number', help='A sequence number that is incremented with each order'), + 'login_number': fields.integer('Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session'), + + '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), + + 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real', + type='float', + digits=0, + string="Ending Balance", + help="Total of closing cash control lines.", + readonly=True), + 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start', + type='float', + digits=0, + string="Starting Balance", + help="Total of opening cash control lines.", + readonly=True), + 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding', + string='Total Cash Transaction', + readonly=True, + help="Total of all paid sale orders"), + 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end', + type='float', + digits=0, + string="Theoretical Closing Balance", + help="Sum of opening balance and transactions.", + readonly=True), + 'cash_register_difference' : fields.related('cash_register_id', 'difference', + type='float', + string='Difference', + help="Difference between the theoretical closing balance and the real closing balance.", + readonly=True), + + 'journal_ids' : fields.related('config_id', 'journal_ids', + type='many2many', + readonly=True, + relation='account.journal', + string='Available Payment Methods'), + 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'), + + 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True), + } + + _defaults = { + 'name' : '/', + 'user_id' : lambda obj, cr, uid, context: uid, + 'state' : 'opening_control', + 'sequence_number': 1, + 'login_number': 0, + } + + _sql_constraints = [ + ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"), + ] + + 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', '=', session.user_id.id) + ] + count = self.search_count(cr, uid, domain, context=context) + if count>1: + return False + return True + + 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 + + _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']), + ] + + def create(self, cr, uid, values, context=None): + context = dict(context or {}) + config_id = values.get('config_id', False) or context.get('default_config_id', False) + if not config_id: + raise UserError(_("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.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, SUPERUSER_ID, [pos_config.id], {'journal_id': jid}, context=context) + else: + raise UserError(_("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) + + journal_proxy.write(cr, SUPERUSER_ID, cashids, {'journal_user': True}) + jobj.write(cr, SUPERUSER_ID, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) + + + pos_config = jobj.browse(cr, uid, config_id, context=context) + + statements = [(0, 0, { + 'journal_id': journal.id, + 'user_id': uid, + 'company_id': pos_config.company_id.id + }) for journal in pos_config.journal_ids] + + values.update({ + 'name': self.pool['ir.sequence'].next_by_code(cr, uid, 'pos.session', context=context), + 'statement_ids': statements, + 'config_id': config_id + }) + + # set the journal_id which should be used by + # account.bank.statement to set the opening balance of the + # newly created bank statement + if pos_config.cash_control: + for journal in pos_config.journal_ids: + if journal.type == 'cash': + context.update({'journal_id': journal.id}) + + return super(pos_session, self).create(cr, uid, values, context=context) + + def unlink(self, cr, uid, ids, context=None): + for obj in self.browse(cr, uid, ids, context=context): + self.pool.get('account.bank.statement').unlink(cr, uid, obj.statement_ids.ids, context=context) + return super(pos_session, self).unlink(cr, uid, ids, context=context) + + def login(self, cr, uid, ids, context=None): + this_record = self.browse(cr, uid, ids[0], context=context) + this_record.write({ + 'login_number': this_record.login_number+1, + }) + + def wkf_action_open(self, cr, uid, ids, context=None): + # 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) + for st in record.statement_ids: + st.button_open() + + return True + + 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 + local_context = dict(context) + for record in self.browse(cr, uid, ids, context=context): + company_id = record.config_id.company_id.id + local_context.update({'force_company': company_id, 'company_id': company_id}) + for st in record.statement_ids: + 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 UserError(_("Your ending balance is too different from the theoretical 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.journal_id.type not in ['bank', 'cash']): + raise UserError(_("The type of the journal for your payment method should be bank or cash ")) + self.pool['account.bank.statement'].button_confirm_bank(cr, SUPERUSER_ID, [st.id], context=local_context) + self._confirm_orders(cr, uid, ids, context=local_context) + self.write(cr, uid, ids, {'state' : 'closed'}, context=local_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): + pos_order_obj = self.pool.get('pos.order') + for session in self.browse(cr, uid, ids, context=context): + company_id = session.config_id.journal_id.company_id.id + local_context = dict(context or {}, force_company=company_id) + order_ids = [order.id for order in session.order_ids if order.state == 'paid'] + + move_id = pos_order_obj._create_account_move(cr, uid, session.start_at, session.name, session.config_id.journal_id.id, company_id, context=context) + + pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=local_context) + + for order in session.order_ids: + if order.state == 'done': + continue + if order.state not in ('paid', 'invoiced'): + raise UserError(_("You cannot confirm all orders of this session, because they have not the 'paid' status")) + else: + pos_order_obj.signal_workflow(cr, uid, [order.id], 'done') + + return True + + def open_frontend_cb(self, cr, uid, ids, context=None): + if not context: + context = {} + if not ids: + return {} + for session in self.browse(cr, uid, ids, context=context): + if session.user_id.id != uid: + raise UserError(_("You cannot use the session of another users. This session is owned by %s. " + "Please first close this one to use this point of sale.") % session.user_id.name) + context.update({'active_id': ids[0]}) + return { + 'type' : 'ir.actions.act_url', + 'target': 'self', + 'url': '/pos/web/', + } diff --git a/addons/point_of_sale/models/product.py b/addons/point_of_sale/models/product.py new file mode 100644 index 00000000000..dd2607c4d60 --- /dev/null +++ b/addons/point_of_sale/models/product.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from openerp.osv import fields, osv +from openerp.tools.translate import _ + + +class product_template(osv.osv): + _inherit = 'product.template' + + _columns = { + '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'), + 'to_weight' : fields.boolean('To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration"), + 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', help="Those categories are used to group similar products for point of sale."), + } + + _defaults = { + 'to_weight' : False, + 'available_in_pos': True, + } + + def unlink(self, cr, uid, ids, context=None): + product_ctx = dict(context or {}, active_test=False) + if self.search_count(cr, uid, [('id', 'in', ids), ('available_in_pos', '=', True)], context=product_ctx): + if self.pool['pos.session'].search_count(cr, uid, [('state', '!=', 'closed')], context=context): + raise osv.except_osv(_('Error!'), + _('You cannot delete a product saleable in point of sale while a session is still opened.')) + return super(product_template, self).unlink(cr, uid, ids, context=context) diff --git a/addons/point_of_sale/models/res_partner.py b/addons/point_of_sale/models/res_partner.py index 53c27c9fd96..b5167b9bea7 100644 --- a/addons/point_of_sale/models/res_partner.py +++ b/addons/point_of_sale/models/res_partner.py @@ -6,4 +6,22 @@ class res_users(osv.osv): _inherit = 'res.partner' _columns = { 'barcode' : fields.char('Barcode', help="BarCode", oldname='ean13'), - } \ No newline at end of file + } + + def create_from_ui(self, cr, uid, partner, context=None): + """ create or modify a partner from the point of sale ui. + partner contains the partner's fields. """ + + #image is a dataurl, get the data after the comma + if partner.get('image',False): + img = partner['image'].split(',')[1] + partner['image'] = img + + if partner.get('id',False): # Modifying existing partner + partner_id = partner['id'] + del partner['id'] + self.write(cr, uid, [partner_id], partner, context=context) + else: + partner_id = self.create(cr, uid, partner, context=context) + + return partner_id diff --git a/addons/point_of_sale/views/account_invoice_view.xml b/addons/point_of_sale/views/account_invoice_view.xml new file mode 100644 index 00000000000..58bfe004dc0 --- /dev/null +++ b/addons/point_of_sale/views/account_invoice_view.xml @@ -0,0 +1,13 @@ + + + + + Invoices + ir.actions.act_window + account.invoice + form + tree,form + [('origin','like','POS')] + + + \ No newline at end of file diff --git a/addons/point_of_sale/views/account_journal_view.xml b/addons/point_of_sale/views/account_journal_view.xml new file mode 100644 index 00000000000..dbd7f41299e --- /dev/null +++ b/addons/point_of_sale/views/account_journal_view.xml @@ -0,0 +1,29 @@ + + + + + Payment Methods + account.journal + form + tree,form + + [('journal_user','=', 1)] + +

+ Click to add a payment method. +

+ Payment methods are defined by accounting journals having the + field PoS Payment Method checked. In order to be useable + from the touchscreen interface, you must set the payment method + on the Point of Sale configuration. +

+
+
+ + + +
+
\ No newline at end of file diff --git a/addons/point_of_sale/views/point_of_sale_report.xml b/addons/point_of_sale/views/point_of_sale_report.xml index 4346c2bc20e..9eee8f2c065 100644 --- a/addons/point_of_sale/views/point_of_sale_report.xml +++ b/addons/point_of_sale/views/point_of_sale_report.xml @@ -56,5 +56,223 @@ name="point_of_sale.report_sessionsummary" file="point_of_sale.report_sessionsummary" /> + + + + + report.trans.pos.user.form + report.transaction.pos + +
+ + + + + + + + + +
+
+
+ + + + Sales by user + report.transaction.pos + + + + + + + + + + + + + + + + + report.transaction.pos.calendar + report.transaction.pos + + + + + + + + + + + report.transaction.pos.graph + report.transaction.pos + + + + + + + + + + + + Sales by day + report.transaction.pos + form + tree,calendar,form,graph + [('date_create','=',time.strftime('%Y-%m-%d'))] + + + + + Sales by month + report.transaction.pos + form + tree,calendar,form,graph + [('date_create','like',time.strftime('%Y-%m'))] + + + + + Sales by user + report.transaction.pos + form + tree,calendar,form,graph + + + + + + report.sales.by.user.pos.form + report.sales.by.user.pos + +
+ + + + + + +
+
+
+ + + + report.sales.by.user.pos.tree + report.sales.by.user.pos + + + + + + + + + + + + report.sales.by.user.pos.calendar + report.sales.by.user.pos + + + + + + + + + + + report.sales.by.user.pos.graph + report.sales.by.user.pos + + + + + + + + + + + Sales by User + report.sales.by.user.pos + form + tree,calendar,form,graph + [('date_order','=',time.strftime('%Y-%m-%d'))] + + + + + + + report.sales.by.user.pos.month.form + report.sales.by.user.pos.month + +
+ + + + + + +
+
+
+ + + + report.sales.by.user.pos.month.tree + report.sales.by.user.pos.month + + + + + + + + + + + + report.sales.by.user.pos.month.calendar + report.sales.by.user.pos.month + + + + + + + + + + + report.sales.by.user.pos.month.graph + report.sales.by.user.pos.month + + + + + + + + + + + + Sales by User Monthly + report.sales.by.user.pos.month + form + tree,calendar,form,graph + [('date_order','like',time.strftime('%Y-%m'))] + + diff --git a/addons/point_of_sale/views/point_of_sale_view.xml b/addons/point_of_sale/views/point_of_sale_view.xml index a895492cc3b..547b8d64285 100644 --- a/addons/point_of_sale/views/point_of_sale_view.xml +++ b/addons/point_of_sale/views/point_of_sale_view.xml @@ -10,989 +10,5 @@ web_icon="point_of_sale,static/description/icon.png" sequence="20"/> - - pos.order - pos.order - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

- Session: - -

- - - - - - - - - - - - - -
- - - -
-
- - - - - - - - -
-
- - - - - - - - - - - - - - -
- -
-
- - - pos.session.tree.view - pos.session - - - - - - - - - - - - - - pos.session.search.view - pos.session - - - - - - - - - - - - - - - - - - - Sessions - ir.actions.act_window - pos.session - form - tree,form - - -

- Click to start a new session. -

- A session is a period of time, usually one day, during which - you sell through the point of sale. The user has to check the - currencies in your cash registers at the beginning and the end - of each session. -

- Note that you may use the menu Your Session - to quickly open a new session. -

-
-
- - - - - pos.order.list.select - pos.order - - - - - - - - - - - - - - - - - - - - - - - Point of Sale - ir.actions.act_window - pos.config - form - kanban,tree,form - - -

- Click to create a new PoS config. -

- Configure at least one Point of Sale to be able to sell through the PoS interface. -

-
-
- - - - - diff --git a/addons/point_of_sale/views/pos_category_view.xml b/addons/point_of_sale/views/pos_category_view.xml new file mode 100644 index 00000000000..9b9932df98a --- /dev/null +++ b/addons/point_of_sale/views/pos_category_view.xml @@ -0,0 +1,59 @@ + + + + + + pos.category.form + pos.category + +
+ + +
+ + + + + +
+
+
+
+
+ + pos.category.tree + pos.category + + + + + + + + + + Pos Product Categories + ir.actions.act_window + pos.category + form + tree,form + + +

+ Click to define a new category. +

+ Categories are used to browse your products through the + touchscreen interface. +

+ If you put a photo on the category and enable the option in the + point of sale configuration, the layout of the touchscreen interface + will automatically be displayed. We suggest not to put a photo + on categories for small (1024x768) screens. +

+
+
+ + + +
+
\ No newline at end of file diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml new file mode 100644 index 00000000000..18b8d0e5c6b --- /dev/null +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -0,0 +1,147 @@ + + + + + pos.config.form.view + pos.config + +
+
+
+ + + + +
+
+
+ + + pos.config.tree.view + pos.config + + + + + + + + + + + pos.config.search.view + pos.config + + + + + + + + + + + + + + Point of Sale + ir.actions.act_window + pos.config + form + tree,form + + + + + + + Point of Sale + ir.actions.act_window + pos.config + form + kanban,tree,form + + +

+ Click to create a new PoS config. +

+ Configure at least one Point of Sale to be able to sell through the PoS interface. +

+
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/addons/point_of_sale/report/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml similarity index 100% rename from addons/point_of_sale/report/pos_order_report_view.xml rename to addons/point_of_sale/views/pos_order_report_view.xml diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml new file mode 100644 index 00000000000..b3fe63c1185 --- /dev/null +++ b/addons/point_of_sale/views/pos_order_view.xml @@ -0,0 +1,279 @@ + + + + + pos.order + pos.order + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Orders + ir.actions.act_window + pos.order + form + tree,form + + [] + +

+ Click to create a new order. +

+ Use this menu to browse previous orders. To record new + orders, you may use the menu Your Session for + the touchscreen interface. +

+
+
+ + Orders + pos.order + + + + + + + + + + + + + + + + + pos.order.list.select + pos.order + + + + + + + + + + + + + + + + + + + + + + pos.order.search.view + pos.order + + + + + + + + + + + + Sale lines + pos.order.line + + + + + + + + + + + + + + + Sale line + pos.order.line + +
+ + + + + + + +
+
+
+ + + Sale line + ir.actions.act_window + pos.order.line + form + tree + + + + + Sale line + ir.actions.act_window + pos.order.line + form + form,tree + + + + + Sale line + ir.actions.act_window + pos.order.line + form + tree + + [('create_date', '>=', time.strftime('%Y-%m-%d 00:00:00')),('create_date', '<=', time.strftime('%Y-%m-%d 23:59:59'))] + + + + POS Sales Lines + pos.order.line + + + + + + + + + + + + All sales lines + ir.actions.act_window + pos.order.line + form + + + + + + + pos.order.list.select + pos.order + + + + + + + + + + + + + + + + + + + + + + + Start Point of Sale + /pos/web/ + self + + + + diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml new file mode 100644 index 00000000000..94f55caeee7 --- /dev/null +++ b/addons/point_of_sale/views/pos_session_view.xml @@ -0,0 +1,179 @@ + + + + + + + + pos.session.form.view + pos.session + +
+
+
+ +
+ + +
+

+ Session: + +

+ + + + + + + + + + + + + +
+ + + +
+
+

+ You can define another list of available currencies on the + Cash Registers tab of the + payment method. +

+
+
+
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + pos.session.tree.view + pos.session + + + + + + + + + + + + + + pos.session.search.view + pos.session + + + + + + + + + + + + + + + + + + + Sessions + ir.actions.act_window + pos.session + form + tree,form + + +

+ Click to start a new session. +

+ A session is a period of time, usually one day, during which + you sell through the point of sale. The user has to check the + currencies in your cash registers at the beginning and the end + of each session. +

+ Note that you may use the menu Your Session + to quickly open a new session. +

+
+
+ + + + + diff --git a/addons/point_of_sale/views/product_view.xml b/addons/point_of_sale/views/product_view.xml new file mode 100644 index 00000000000..34623e184fe --- /dev/null +++ b/addons/point_of_sale/views/product_view.xml @@ -0,0 +1,59 @@ + + + + + Products + ir.actions.act_window + product.template + form + tree,form,kanban + + + + + +

+ Click to add a new product. +

+ You must define a product for everything you sell through + the point of sale interface. +

+ Do not forget to set the price and the point of sale category + in which it should appear. If a product has no point of sale + category, you can not sell it through the point of sale + interface. +

+
+
+ + + + product.template.form.inherit + product.template + + + + + + + + + + + + + product.template.tree.inherit + product.template + + + + + + + + +
+
\ No newline at end of file diff --git a/addons/point_of_sale/wizard/pos_payment.py b/addons/point_of_sale/wizard/pos_payment.py index 40d8ff18a55..f609a5ffee5 100644 --- a/addons/point_of_sale/wizard/pos_payment.py +++ b/addons/point_of_sale/wizard/pos_payment.py @@ -7,27 +7,10 @@ from openerp.osv import osv, fields from openerp.tools.translate import _ -class account_journal(osv.osv): - _inherit = 'account.journal' - - def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): - if not context: - context = {} - session_id = context.get('pos_session_id', False) or False - - if session_id: - session = self.pool.get('pos.session').browse(cr, uid, session_id, context=context) - - if session: - journal_ids = [journal.id for journal in session.config_id.journal_ids] - args += [('id', 'in', journal_ids)] - - return super(account_journal, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count) - - class pos_make_payment(osv.osv_memory): _name = 'pos.make.payment' _description = 'Point of Sale Payment' + def check(self, cr, uid, ids, context=None): """Check the order: if the order is not paid: continue payment,