# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.exceptions import UserError, ValidationError class PosSession(models.Model): _name = 'pos.session' _order = 'id desc' POS_SESSION_STATE = [ ('opening_control', 'Opening Control'), # method action_pos_session_open ('opened', 'In Progress'), # method action_pos_session_closing_control ('closing_control', 'Closing Control'), # method action_pos_session_close ('closed', 'Closed & Posted'), ] def _confirm_orders(self): for session in self: company_id = session.config_id.journal_id.company_id.id orders = session.order_ids.filtered(lambda order: order.state == 'paid') journal_id = self.env['ir.config_parameter'].sudo().get_param( 'pos.closing.journal_id_%s' % company_id, default=session.config_id.journal_id.id) if not journal_id: raise UserError(_("You have to set a Sale Journal for the POS:%s") % (session.config_id.name,)) move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, int(journal_id), company_id) orders.with_context(force_company=company_id)._create_account_move_line(session, move) for order in session.order_ids.filtered(lambda o: o.state not in ['done', 'invoiced']): if order.state not in ('paid'): raise UserError( _("You cannot confirm all orders of this session, because they have not the 'paid' status.\n" "{reference} is in state {state}, total amount: {total}, paid: {paid}").format( reference=order.pos_reference or order.name, state=order.state, total=order.amount_total, paid=order.amount_paid, )) order.action_pos_order_done() orders = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done']) orders.sudo()._reconcile_payments() config_id = fields.Many2one( 'pos.config', string='Point of Sale', help="The physical point of sale you will use.", required=True, index=True) name = fields.Char(string='Session ID', required=True, readonly=True, default='/') user_id = fields.Many2one( 'res.users', string='Responsible', required=True, index=True, readonly=True, states={'opening_control': [('readonly', False)]}, default=lambda self: self.env.uid) currency_id = fields.Many2one('res.currency', related='config_id.currency_id', string="Currency") start_at = fields.Datetime(string='Opening Date', readonly=True) stop_at = fields.Datetime(string='Closing Date', readonly=True, copy=False) state = fields.Selection( POS_SESSION_STATE, string='Status', required=True, readonly=True, index=True, copy=False, default='opening_control') sequence_number = fields.Integer(string='Order Sequence Number', help='A sequence number that is incremented with each order', default=1) login_number = fields.Integer(string='Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session', default=0) cash_control = fields.Boolean(compute='_compute_cash_all', string='Has Cash Control') cash_journal_id = fields.Many2one('account.journal', compute='_compute_cash_all', string='Cash Journal', store=True) cash_register_id = fields.Many2one('account.bank.statement', compute='_compute_cash_all', string='Cash Register', store=True) cash_register_balance_end_real = fields.Monetary( related='cash_register_id.balance_end_real', string="Ending Balance", help="Total of closing cash control lines.", readonly=True) cash_register_balance_start = fields.Monetary( related='cash_register_id.balance_start', string="Starting Balance", help="Total of opening cash control lines.", readonly=True) cash_register_total_entry_encoding = fields.Monetary( related='cash_register_id.total_entry_encoding', string='Total Cash Transaction', readonly=True, help="Total of all paid sales orders") cash_register_balance_end = fields.Monetary( related='cash_register_id.balance_end', digits=0, string="Theoretical Closing Balance", help="Sum of opening balance and transactions.", readonly=True) cash_register_difference = fields.Monetary( related='cash_register_id.difference', string='Difference', help="Difference between the theoretical closing balance and the real closing balance.", readonly=True) journal_ids = fields.Many2many( 'account.journal', related='config_id.journal_ids', readonly=True, string='Available Payment Methods') order_ids = fields.One2many('pos.order', 'session_id', string='Orders') statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Bank Statement', readonly=True) picking_count = fields.Integer(compute='_compute_picking_count') rescue = fields.Boolean(string='Recovery Session', help="Auto-generated session for orphan orders, ignored in constraints", readonly=True, copy=False) _sql_constraints = [('uniq_name', 'unique(name)', "The name of this POS Session must be unique !")] @api.multi def _compute_picking_count(self): for pos in self: pickings = pos.order_ids.mapped('picking_id').filtered(lambda x: x.state != 'done') pos.picking_count = len(pickings.ids) @api.multi def action_stock_picking(self): pickings = self.order_ids.mapped('picking_id').filtered(lambda x: x.state != 'done') action_picking = self.env.ref('stock.action_picking_tree_ready') action = action_picking.read()[0] action['context'] = {} action['domain'] = [('id', 'in', pickings.ids)] return action @api.depends('config_id', 'statement_ids') def _compute_cash_all(self): for session in self: session.cash_journal_id = session.cash_register_id = session.cash_control = False if session.config_id.cash_control: for statement in session.statement_ids: if statement.journal_id.type == 'cash': session.cash_control = True session.cash_journal_id = statement.journal_id.id session.cash_register_id = statement.id if not session.cash_control and session.state != 'closed': raise UserError(_("Cash control can only be applied to cash journals.")) @api.constrains('user_id', 'state') def _check_unicity(self): # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user if self.search_count([ ('state', 'not in', ('closed', 'closing_control')), ('user_id', '=', self.user_id.id), ('rescue', '=', False) ]) > 1: raise ValidationError(_("You cannot create two active sessions with the same responsible!")) @api.constrains('config_id') def _check_pos_config(self): if self.search_count([ ('state', '!=', 'closed'), ('config_id', '=', self.config_id.id), ('rescue', '=', False) ]) > 1: raise ValidationError(_("Another session is already opened for this point of sale.")) @api.model def create(self, values): config_id = values.get('config_id') or self.env.context.get('default_config_id') 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. pos_config = self.env['pos.config'].browse(config_id) ctx = dict(self.env.context, company_id=pos_config.company_id.id) if not pos_config.journal_id: default_journals = pos_config.with_context(ctx).default_get(['journal_id', 'invoice_journal_id']) if (not default_journals.get('journal_id') or not default_journals.get('invoice_journal_id')): raise UserError(_("Unable to open the session. You have to assign a sales journal to your point of sale.")) pos_config.with_context(ctx).sudo().write({ 'journal_id': default_journals['journal_id'], 'invoice_journal_id': default_journals['invoice_journal_id']}) # define some cash journal if no payment method exists if not pos_config.journal_ids: Journal = self.env['account.journal'] journals = Journal.with_context(ctx).search([('journal_user', '=', True), ('type', '=', 'cash')]) if not journals: journals = Journal.with_context(ctx).search([('type', '=', 'cash')]) if not journals: journals = Journal.with_context(ctx).search([('journal_user', '=', True)]) journals.sudo().write({'journal_user': True}) pos_config.sudo().write({'journal_ids': [(6, 0, journals.ids)]}) pos_name = self.env['ir.sequence'].with_context(ctx).next_by_code('pos.session') if values.get('name'): pos_name += ' ' + values['name'] statements = [] ABS = self.env['account.bank.statement'] uid = SUPERUSER_ID if self.env.user.has_group('point_of_sale.group_pos_user') else self.env.user.id for journal in pos_config.journal_ids: # set the journal_id which should be used by # account.bank.statement to set the opening balance of the # newly created bank statement ctx['journal_id'] = journal.id if pos_config.cash_control and journal.type == 'cash' else False st_values = { 'journal_id': journal.id, 'user_id': self.env.user.id, 'name': pos_name } statements.append(ABS.with_context(ctx).sudo(uid).create(st_values).id) values.update({ 'name': pos_name, 'statement_ids': [(6, 0, statements)], 'config_id': config_id }) res = super(PosSession, self.with_context(ctx).sudo(uid)).create(values) if not pos_config.cash_control: res.action_pos_session_open() return res @api.multi def unlink(self): for session in self.filtered(lambda s: s.statement_ids): session.statement_ids.unlink() return super(PosSession, self).unlink() @api.multi def login(self): self.ensure_one() self.write({ 'login_number': self.login_number + 1, }) @api.multi def action_pos_session_open(self): # second browse because we need to refetch the data from the DB for cash_register_id # we only open sessions that haven't already been opened for session in self.filtered(lambda session: session.state == 'opening_control'): values = {} if not session.start_at: values['start_at'] = fields.Datetime.now() values['state'] = 'opened' session.write(values) session.statement_ids.button_open() return True @api.multi def action_pos_session_closing_control(self): self._check_pos_session_balance() for session in self: session.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()}) if not session.config_id.cash_control: session.action_pos_session_close() @api.multi def _check_pos_session_balance(self): for session in self: for statement in session.statement_ids: if (statement != session.cash_register_id) and (statement.balance_end != statement.balance_end_real): statement.write({'balance_end_real': statement.balance_end}) @api.multi def action_pos_session_validate(self): self._check_pos_session_balance() self.action_pos_session_close() @api.multi def action_pos_session_close(self): # Close CashBox for session in self: company_id = session.config_id.company_id.id ctx = dict(self.env.context, force_company=company_id, company_id=company_id) for st in session.statement_ids: if abs(st.difference) > st.journal_id.amount_authorized_diff: # The pos manager can close statements with maximums. if not self.user_has_groups("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 ")) st.with_context(ctx).sudo().button_confirm_bank() self.with_context(ctx)._confirm_orders() self.write({'state': 'closed'}) return { 'type': 'ir.actions.client', 'name': 'Point of Sale Menu', 'tag': 'reload', 'params': {'menu_id': self.env.ref('point_of_sale.menu_point_root').id}, } @api.multi def open_frontend_cb(self): if not self.ids: return {} for session in self.filtered(lambda s: s.user_id.id != self.env.uid): raise UserError(_("You cannot use the session of another user. This session is owned by %s. " "Please first close this one to use this point of sale.") % session.user_id.name) return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/pos/web/', } @api.multi def open_cashbox(self): self.ensure_one() context = dict(self._context) balance_type = context.get('balance') or 'start' context['bank_statement_id'] = self.cash_register_id.id context['balance'] = balance_type context['default_pos_id'] = self.config_id.id action = { 'name': _('Cash Control'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.bank.statement.cashbox', 'view_id': self.env.ref('account.view_account_bnk_stmt_cashbox').id, 'type': 'ir.actions.act_window', 'context': context, 'target': 'new' } cashbox_id = None if balance_type == 'start': cashbox_id = self.cash_register_id.cashbox_start_id.id else: cashbox_id = self.cash_register_id.cashbox_end_id.id if cashbox_id: action['res_id'] = cashbox_id return action