flectra/addons/point_of_sale/models/pos_session.py

336 lines
15 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2018-01-16 11:34:37 +01:00
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
2018-01-16 11:34:37 +01:00
from flectra import api, fields, models, SUPERUSER_ID, _
from flectra.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_to_reconcile = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'] and order.partner_id)
orders_to_reconcile.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