For now, when you are selling products in the point of sales, the stock is affected once the order is validated or the session closed. But in some case, you don't have the product in your store, and you have to plan a delivery for the order. To be able to plan a delivery, you can now choose the warehouse in POS Config and the routes of your warehouse will be applied. You can also set a specific rule on your POS Config if you've enabled the multi-step-routes in the inventory module closes odoo/odoo#58506 Task-id: 2082104 Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
1155 lines
59 KiB
Python
1155 lines
59 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
from datetime import timedelta
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.tools import float_is_zero, float_compare
|
|
|
|
|
|
class PosSession(models.Model):
|
|
_name = 'pos.session'
|
|
_order = 'id desc'
|
|
_description = 'Point of Sale Session'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
|
|
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'),
|
|
]
|
|
|
|
company_id = fields.Many2one('res.company', related='config_id.company_id', string="Company", readonly=True)
|
|
|
|
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='Opened By',
|
|
required=True,
|
|
index=True,
|
|
readonly=True,
|
|
states={'opening_control': [('readonly', False)]},
|
|
default=lambda self: self.env.uid,
|
|
ondelete='restrict')
|
|
currency_id = fields.Many2one('res.currency', related='config_id.currency_id', string="Currency", readonly=False)
|
|
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', compute_sudo=True)
|
|
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(
|
|
compute='_compute_cash_balance',
|
|
string='Total Cash Transaction',
|
|
readonly=True,
|
|
help="Total of all paid sales orders")
|
|
cash_register_balance_end = fields.Monetary(
|
|
compute='_compute_cash_balance',
|
|
string="Theoretical Closing Balance",
|
|
help="Sum of opening balance and transactions.",
|
|
readonly=True)
|
|
cash_register_difference = fields.Monetary(
|
|
compute='_compute_cash_balance',
|
|
string='Before Closing Difference',
|
|
help="Difference between the theoretical closing balance and the real closing balance.",
|
|
readonly=True)
|
|
cash_real_difference = fields.Monetary(string='Difference', readonly=True)
|
|
cash_real_transaction = fields.Monetary(string='Transaction', readonly=True)
|
|
cash_real_expected = fields.Monetary(string="Expected", readonly=True)
|
|
|
|
order_ids = fields.One2many('pos.order', 'session_id', string='Orders')
|
|
order_count = fields.Integer(compute='_compute_order_count')
|
|
statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Cash Statements', readonly=True)
|
|
failed_pickings = fields.Boolean(compute='_compute_picking_count')
|
|
picking_count = fields.Integer(compute='_compute_picking_count')
|
|
picking_ids = fields.One2many('stock.picking', 'pos_session_id')
|
|
rescue = fields.Boolean(string='Recovery Session',
|
|
help="Auto-generated session for orphan orders, ignored in constraints",
|
|
readonly=True,
|
|
copy=False)
|
|
move_id = fields.Many2one('account.move', string='Journal Entry')
|
|
payment_method_ids = fields.Many2many('pos.payment.method', related='config_id.payment_method_ids', string='Payment Methods')
|
|
total_payments_amount = fields.Float(compute='_compute_total_payments_amount', string='Total Payments Amount')
|
|
is_in_company_currency = fields.Boolean('Is Using Company Currency', compute='_compute_is_in_company_currency')
|
|
update_stock_at_closing = fields.Boolean('Stock should be updated at closing')
|
|
|
|
_sql_constraints = [('uniq_name', 'unique(name)', "The name of this POS Session must be unique !")]
|
|
|
|
@api.depends('currency_id', 'company_id.currency_id')
|
|
def _compute_is_in_company_currency(self):
|
|
for session in self:
|
|
session.is_in_company_currency = session.currency_id == session.company_id.currency_id
|
|
|
|
@api.depends('payment_method_ids', 'order_ids', 'cash_register_balance_start', 'cash_register_id')
|
|
def _compute_cash_balance(self):
|
|
for session in self:
|
|
cash_payment_method = session.payment_method_ids.filtered('is_cash_count')[:1]
|
|
if cash_payment_method:
|
|
total_cash_payment = sum(session.order_ids.mapped('payment_ids').filtered(lambda payment: payment.payment_method_id == cash_payment_method).mapped('amount'))
|
|
session.cash_register_total_entry_encoding = session.cash_register_id.total_entry_encoding + (
|
|
0.0 if session.state == 'closed' else total_cash_payment
|
|
)
|
|
session.cash_register_balance_end = session.cash_register_balance_start + session.cash_register_total_entry_encoding
|
|
session.cash_register_difference = session.cash_register_balance_end_real - session.cash_register_balance_end
|
|
else:
|
|
session.cash_register_total_entry_encoding = 0.0
|
|
session.cash_register_balance_end = 0.0
|
|
session.cash_register_difference = 0.0
|
|
|
|
@api.depends('order_ids.payment_ids.amount')
|
|
def _compute_total_payments_amount(self):
|
|
for session in self:
|
|
session.total_payments_amount = sum(session.order_ids.mapped('payment_ids.amount'))
|
|
|
|
def _compute_order_count(self):
|
|
orders_data = self.env['pos.order'].read_group([('session_id', 'in', self.ids)], ['session_id'], ['session_id'])
|
|
sessions_data = {order_data['session_id'][0]: order_data['session_id_count'] for order_data in orders_data}
|
|
for session in self:
|
|
session.order_count = sessions_data.get(session.id, 0)
|
|
|
|
@api.depends('picking_ids', 'picking_ids.state')
|
|
def _compute_picking_count(self):
|
|
for session in self:
|
|
session.picking_count = len(session.picking_ids.ids)
|
|
session.failed_pickings = bool(session.picking_ids.filtered(lambda p: p.state != 'done'))
|
|
|
|
def action_stock_picking(self):
|
|
self.ensure_one()
|
|
action = self.env['ir.actions.act_window']._for_xml_id('stock.action_picking_tree_ready')
|
|
action['context'] = {}
|
|
action['domain'] = [('id', 'in', self.picking_ids.ids)]
|
|
return action
|
|
|
|
@api.depends('config_id', 'statement_ids', 'payment_method_ids')
|
|
def _compute_cash_all(self):
|
|
# Only one cash register is supported by point_of_sale.
|
|
for session in self:
|
|
session.cash_journal_id = session.cash_register_id = session.cash_control = False
|
|
cash_payment_methods = session.payment_method_ids.filtered('is_cash_count')
|
|
if not cash_payment_methods:
|
|
continue
|
|
for statement in session.statement_ids:
|
|
if statement.journal_id == cash_payment_methods[0].cash_journal_id:
|
|
session.cash_control = session.config_id.cash_control
|
|
session.cash_journal_id = statement.journal_id.id
|
|
session.cash_register_id = statement.id
|
|
break # stop iteration after finding the cash journal
|
|
|
|
@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.constrains('start_at')
|
|
def _check_start_date(self):
|
|
for record in self:
|
|
company = record.config_id.journal_id.company_id
|
|
start_date = record.start_at.date()
|
|
if (company.period_lock_date and start_date <= company.period_lock_date) or (company.fiscalyear_lock_date and start_date <= company.fiscalyear_lock_date):
|
|
raise ValidationError(_("You cannot create a session before the accounting lock date."))
|
|
|
|
@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)
|
|
|
|
pos_name = self.env['ir.sequence'].with_context(ctx).next_by_code('pos.session')
|
|
if values.get('name'):
|
|
pos_name += ' ' + values['name']
|
|
|
|
cash_payment_methods = pos_config.payment_method_ids.filtered(lambda pm: pm.is_cash_count)
|
|
statement_ids = self.env['account.bank.statement']
|
|
if self.user_has_groups('point_of_sale.group_pos_user'):
|
|
statement_ids = statement_ids.sudo()
|
|
for cash_journal in cash_payment_methods.mapped('cash_journal_id'):
|
|
ctx['journal_id'] = cash_journal.id if pos_config.cash_control and cash_journal.type == 'cash' else False
|
|
st_values = {
|
|
'journal_id': cash_journal.id,
|
|
'user_id': self.env.user.id,
|
|
'name': pos_name,
|
|
}
|
|
statement_ids |= statement_ids.with_context(ctx).create(st_values)
|
|
|
|
update_stock_at_closing = pos_config.company_id.point_of_sale_update_stock_quantities == "closing"
|
|
|
|
values.update({
|
|
'name': pos_name,
|
|
'statement_ids': [(6, 0, statement_ids.ids)],
|
|
'config_id': config_id,
|
|
'update_stock_at_closing': update_stock_at_closing,
|
|
})
|
|
|
|
if self.user_has_groups('point_of_sale.group_pos_user'):
|
|
res = super(PosSession, self.with_context(ctx).sudo()).create(values)
|
|
else:
|
|
res = super(PosSession, self.with_context(ctx)).create(values)
|
|
res.action_pos_session_open()
|
|
|
|
return res
|
|
|
|
def unlink(self):
|
|
for session in self.filtered(lambda s: s.statement_ids):
|
|
session.statement_ids.unlink()
|
|
return super(PosSession, self).unlink()
|
|
|
|
def login(self):
|
|
self.ensure_one()
|
|
login_number = self.login_number + 1
|
|
self.write({
|
|
'login_number': login_number,
|
|
})
|
|
return login_number
|
|
|
|
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 in ('new_session', 'opening_control')):
|
|
values = {}
|
|
if not session.start_at:
|
|
values['start_at'] = fields.Datetime.now()
|
|
if session.config_id.cash_control and not session.rescue:
|
|
last_sessions = self.env['pos.session'].search([('config_id', '=', self.config_id.id)]).ids
|
|
# last session includes the new one already.
|
|
if len(last_sessions) > 1:
|
|
self.cash_register_id.balance_start = self.env['pos.session'].browse(last_sessions[1]).cash_register_id.balance_end_real
|
|
values['state'] = 'opening_control'
|
|
else:
|
|
values['state'] = 'opened'
|
|
session.write(values)
|
|
return True
|
|
|
|
def action_pos_session_closing_control(self):
|
|
self._check_pos_session_balance()
|
|
for session in self:
|
|
if session.state == 'closed':
|
|
raise UserError(_('This session is already closed.'))
|
|
session.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()})
|
|
if not session.config_id.cash_control:
|
|
session.action_pos_session_close()
|
|
|
|
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})
|
|
|
|
def action_pos_session_validate(self):
|
|
self._check_pos_session_balance()
|
|
return self.action_pos_session_close()
|
|
|
|
def action_pos_session_close(self):
|
|
# Session without cash payment method will not have a cash register.
|
|
# However, there could be other payment methods, thus, session still
|
|
# needs to be validated.
|
|
if not self.cash_register_id:
|
|
return self._validate_session()
|
|
|
|
if self.cash_control and abs(self.cash_register_difference) > self.config_id.amount_authorized_diff:
|
|
# Only pos manager can close statements with cash_register_difference greater than amount_authorized_diff.
|
|
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."
|
|
) % (self.cash_register_difference, self.config_id.amount_authorized_diff))
|
|
else:
|
|
return self._warning_balance_closing()
|
|
else:
|
|
return self._validate_session()
|
|
|
|
def _validate_session(self):
|
|
self.ensure_one()
|
|
if self.order_ids:
|
|
self.cash_real_transaction = self.cash_register_total_entry_encoding
|
|
self.cash_real_expected = self.cash_register_balance_end
|
|
self.cash_real_difference = self.cash_register_difference
|
|
if self.state == 'closed':
|
|
raise UserError(_('This session is already closed.'))
|
|
self._check_if_no_draft_orders()
|
|
if self.update_stock_at_closing:
|
|
self._create_picking_at_end_of_session()
|
|
# Users without any accounting rights won't be able to create the journal entry. If this
|
|
# case, switch to sudo for creation and posting.
|
|
sudo = False
|
|
if (
|
|
not self.env['account.move'].check_access_rights('create', raise_exception=False)
|
|
and self.user_has_groups('point_of_sale.group_pos_user')
|
|
):
|
|
sudo = True
|
|
self.sudo().with_company(self.company_id)._create_account_move()
|
|
else:
|
|
self.with_company(self.company_id)._create_account_move()
|
|
if self.move_id.line_ids:
|
|
# Set the uninvoiced orders' state to 'done'
|
|
self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'})
|
|
else:
|
|
self.move_id.unlink()
|
|
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},
|
|
}
|
|
|
|
def _create_picking_at_end_of_session(self):
|
|
self.ensure_one()
|
|
lines_grouped_by_dest_location = {}
|
|
picking_type = self.config_id.picking_type_id
|
|
|
|
if not picking_type or not picking_type.default_location_dest_id:
|
|
session_destination_id = self.env['stock.warehouse']._get_partner_locations()[0].id
|
|
else:
|
|
session_destination_id = picking_type.default_location_dest_id.id
|
|
|
|
for order in self.order_ids:
|
|
if (order.company_id.anglo_saxon_accounting and order.to_invoice) or order.to_ship:
|
|
continue
|
|
destination_id = order.partner_id.property_stock_customer.id or session_destination_id
|
|
if destination_id in lines_grouped_by_dest_location:
|
|
lines_grouped_by_dest_location[destination_id] |= order.lines
|
|
else:
|
|
lines_grouped_by_dest_location[destination_id] = order.lines
|
|
|
|
for location_dest_id, lines in lines_grouped_by_dest_location.items():
|
|
pickings = self.env['stock.picking']._create_picking_from_pos_order_lines(location_dest_id, lines, picking_type)
|
|
pickings.write({'pos_session_id': self.id, 'origin': self.name})
|
|
|
|
def _create_balancing_line(self, data):
|
|
imbalance_amount = 0
|
|
for line in self.move_id.line_ids:
|
|
# it is an excess debit so it should be credited
|
|
imbalance_amount += line.debit - line.credit
|
|
|
|
if (not float_is_zero(imbalance_amount, precision_rounding=self.currency_id.rounding)):
|
|
balancing_vals = self._prepare_balancing_line_vals(imbalance_amount, self.move_id)
|
|
MoveLine = data.get('MoveLine')
|
|
MoveLine.create(balancing_vals)
|
|
|
|
return data
|
|
|
|
def _prepare_balancing_line_vals(self, imbalance_amount, move):
|
|
account = self._get_balancing_account()
|
|
partial_vals = {
|
|
'name': _('Difference at closing PoS session'),
|
|
'account_id': account.id,
|
|
'move_id': move.id,
|
|
'partner_id': False,
|
|
}
|
|
# `imbalance_amount` is already in terms of company currency so it is the amount_converted
|
|
# param when calling `_credit_amounts`. amount param will be the converted value of
|
|
# `imbalance_amount` from company currency to the session currency.
|
|
imbalance_amount_session = 0
|
|
if (not self.is_in_company_currency):
|
|
imbalance_amount_session = self.company_id.currency_id._convert(imbalance_amount, self.currency_id, self.company_id, fields.Date.context_today(self))
|
|
return self._credit_amounts(partial_vals, imbalance_amount_session, imbalance_amount)
|
|
|
|
def _get_balancing_account(self):
|
|
return self.company_id.account_default_pos_receivable_account_id or self.env['ir.property'].get('property_account_receivable_id', 'res.partner')
|
|
|
|
def _create_account_move(self):
|
|
""" Create account.move and account.move.line records for this session.
|
|
|
|
Side-effects include:
|
|
- setting self.move_id to the created account.move record
|
|
- creating and validating account.bank.statement for cash payments
|
|
- reconciling cash receivable lines, invoice receivable lines and stock output lines
|
|
"""
|
|
journal = self.config_id.journal_id
|
|
# Passing default_journal_id for the calculation of default currency of account move
|
|
# See _get_default_currency in the account/account_move.py.
|
|
account_move = self.env['account.move'].with_context(default_journal_id=journal.id).create({
|
|
'journal_id': journal.id,
|
|
'date': fields.Date.context_today(self),
|
|
'ref': self.name,
|
|
})
|
|
self.write({'move_id': account_move.id})
|
|
|
|
data = {}
|
|
data = self._accumulate_amounts(data)
|
|
data = self._create_non_reconciliable_move_lines(data)
|
|
data = self._create_cash_statement_lines_and_cash_move_lines(data)
|
|
data = self._create_invoice_receivable_lines(data)
|
|
data = self._create_stock_output_lines(data)
|
|
data = self._create_balancing_line(data)
|
|
|
|
if account_move.line_ids:
|
|
account_move._post()
|
|
|
|
data = self._reconcile_account_move_lines(data)
|
|
|
|
def _accumulate_amounts(self, data):
|
|
# Accumulate the amounts for each accounting lines group
|
|
# Each dict maps `key` -> `amounts`, where `key` is the group key.
|
|
# E.g. `combine_receivables` is derived from pos.payment records
|
|
# in the self.order_ids with group key of the `payment_method_id`
|
|
# field of the pos.payment record.
|
|
amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0}
|
|
tax_amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0, 'base_amount': 0.0, 'base_amount_converted': 0.0}
|
|
split_receivables = defaultdict(amounts)
|
|
split_receivables_cash = defaultdict(amounts)
|
|
combine_receivables = defaultdict(amounts)
|
|
combine_receivables_cash = defaultdict(amounts)
|
|
invoice_receivables = defaultdict(amounts)
|
|
sales = defaultdict(amounts)
|
|
taxes = defaultdict(tax_amounts)
|
|
stock_expense = defaultdict(amounts)
|
|
stock_return = defaultdict(amounts)
|
|
stock_output = defaultdict(amounts)
|
|
rounding_difference = {'amount': 0.0, 'amount_converted': 0.0}
|
|
# Track the receivable lines of the invoiced orders' account moves for reconciliation
|
|
# These receivable lines are reconciled to the corresponding invoice receivable lines
|
|
# of this session's move_id.
|
|
order_account_move_receivable_lines = defaultdict(lambda: self.env['account.move.line'])
|
|
rounded_globally = self.company_id.tax_calculation_rounding_method == 'round_globally'
|
|
for order in self.order_ids:
|
|
# Combine pos receivable lines
|
|
# Separate cash payments for cash reconciliation later.
|
|
for payment in order.payment_ids:
|
|
amount, date = payment.amount, payment.payment_date
|
|
if payment.payment_method_id.split_transactions:
|
|
if payment.payment_method_id.is_cash_count:
|
|
split_receivables_cash[payment] = self._update_amounts(split_receivables_cash[payment], {'amount': amount}, date)
|
|
else:
|
|
split_receivables[payment] = self._update_amounts(split_receivables[payment], {'amount': amount}, date)
|
|
else:
|
|
key = payment.payment_method_id
|
|
if payment.payment_method_id.is_cash_count:
|
|
combine_receivables_cash[key] = self._update_amounts(combine_receivables_cash[key], {'amount': amount}, date)
|
|
else:
|
|
combine_receivables[key] = self._update_amounts(combine_receivables[key], {'amount': amount}, date)
|
|
|
|
if order.is_invoiced:
|
|
# Combine invoice receivable lines
|
|
key = order.partner_id.property_account_receivable_id.id
|
|
if self.config_id.cash_rounding:
|
|
invoice_receivables[key] = self._update_amounts(invoice_receivables[key], {'amount': order.amount_paid}, order.date_order)
|
|
else:
|
|
invoice_receivables[key] = self._update_amounts(invoice_receivables[key], {'amount': order.amount_total}, order.date_order)
|
|
# side loop to gather receivable lines by account for reconciliation
|
|
for move_line in order.account_move.line_ids.filtered(lambda aml: aml.account_id.internal_type == 'receivable' and not aml.reconciled):
|
|
order_account_move_receivable_lines[move_line.account_id.id] |= move_line
|
|
else:
|
|
order_taxes = defaultdict(tax_amounts)
|
|
for order_line in order.lines:
|
|
line = self._prepare_line(order_line)
|
|
# Combine sales/refund lines
|
|
sale_key = (
|
|
# account
|
|
line['income_account_id'],
|
|
# sign
|
|
-1 if line['amount'] < 0 else 1,
|
|
# for taxes
|
|
tuple((tax['id'], tax['account_id'], tax['tax_repartition_line_id']) for tax in line['taxes']),
|
|
line['base_tags'],
|
|
)
|
|
sales[sale_key] = self._update_amounts(sales[sale_key], {'amount': line['amount']}, line['date_order'])
|
|
# Combine tax lines
|
|
for tax in line['taxes']:
|
|
tax_key = (tax['account_id'], tax['tax_repartition_line_id'], tax['id'], tuple(tax['tag_ids']))
|
|
order_taxes[tax_key] = self._update_amounts(
|
|
order_taxes[tax_key],
|
|
{'amount': tax['amount'], 'base_amount': tax['base']},
|
|
tax['date_order'],
|
|
round=not rounded_globally
|
|
)
|
|
for tax_key, amounts in order_taxes.items():
|
|
if rounded_globally:
|
|
amounts = self._round_amounts(amounts)
|
|
for amount_key, amount in amounts.items():
|
|
taxes[tax_key][amount_key] += amount
|
|
|
|
if self.company_id.anglo_saxon_accounting and order.picking_ids.ids:
|
|
# Combine stock lines
|
|
stock_moves = self.env['stock.move'].sudo().search([
|
|
('picking_id', 'in', order.picking_ids.ids),
|
|
('company_id.anglo_saxon_accounting', '=', True),
|
|
('product_id.categ_id.property_valuation', '=', 'real_time')
|
|
])
|
|
for move in stock_moves:
|
|
exp_key = move.product_id._get_product_accounts()['expense']
|
|
out_key = move.product_id.categ_id.property_stock_account_output_categ_id
|
|
amount = -sum(move.sudo().stock_valuation_layer_ids.mapped('value'))
|
|
stock_expense[exp_key] = self._update_amounts(stock_expense[exp_key], {'amount': amount}, move.picking_id.date, force_company_currency=True)
|
|
if move.location_id.usage == 'customer':
|
|
stock_return[out_key] = self._update_amounts(stock_return[out_key], {'amount': amount}, move.picking_id.date, force_company_currency=True)
|
|
else:
|
|
stock_output[out_key] = self._update_amounts(stock_output[out_key], {'amount': amount}, move.picking_id.date, force_company_currency=True)
|
|
|
|
if self.config_id.cash_rounding:
|
|
diff = order.amount_paid - order.amount_total
|
|
rounding_difference = self._update_amounts(rounding_difference, {'amount': diff}, order.date_order)
|
|
|
|
# Increasing current partner's customer_rank
|
|
partners = (order.partner_id | order.partner_id.commercial_partner_id)
|
|
partners._increase_rank('customer_rank')
|
|
|
|
if self.company_id.anglo_saxon_accounting:
|
|
global_session_pickings = self.picking_ids.filtered(lambda p: not p.pos_order_id)
|
|
if global_session_pickings:
|
|
stock_moves = self.env['stock.move'].sudo().search([
|
|
('picking_id', 'in', global_session_pickings.ids),
|
|
('company_id.anglo_saxon_accounting', '=', True),
|
|
('product_id.categ_id.property_valuation', '=', 'real_time'),
|
|
])
|
|
for move in stock_moves:
|
|
exp_key = move.product_id._get_product_accounts()['expense']
|
|
out_key = move.product_id.categ_id.property_stock_account_output_categ_id
|
|
amount = -sum(move.stock_valuation_layer_ids.mapped('value'))
|
|
stock_expense[exp_key] = self._update_amounts(stock_expense[exp_key], {'amount': amount}, move.picking_id.date)
|
|
if move.location_id.usage == 'customer':
|
|
stock_return[out_key] = self._update_amounts(stock_return[out_key], {'amount': amount}, move.picking_id.date)
|
|
else:
|
|
stock_output[out_key] = self._update_amounts(stock_output[out_key], {'amount': amount}, move.picking_id.date)
|
|
MoveLine = self.env['account.move.line'].with_context(check_move_validity=False)
|
|
|
|
data.update({
|
|
'taxes': taxes,
|
|
'sales': sales,
|
|
'stock_expense': stock_expense,
|
|
'split_receivables': split_receivables,
|
|
'combine_receivables': combine_receivables,
|
|
'split_receivables_cash': split_receivables_cash,
|
|
'combine_receivables_cash': combine_receivables_cash,
|
|
'invoice_receivables': invoice_receivables,
|
|
'stock_return': stock_return,
|
|
'stock_output': stock_output,
|
|
'order_account_move_receivable_lines': order_account_move_receivable_lines,
|
|
'rounding_difference': rounding_difference,
|
|
'MoveLine': MoveLine
|
|
})
|
|
return data
|
|
|
|
def _create_non_reconciliable_move_lines(self, data):
|
|
# Create account.move.line records for
|
|
# - sales
|
|
# - taxes
|
|
# - stock expense
|
|
# - non-cash split receivables (not for automatic reconciliation)
|
|
# - non-cash combine receivables (not for automatic reconciliation)
|
|
taxes = data.get('taxes')
|
|
sales = data.get('sales')
|
|
stock_expense = data.get('stock_expense')
|
|
split_receivables = data.get('split_receivables')
|
|
combine_receivables = data.get('combine_receivables')
|
|
rounding_difference = data.get('rounding_difference')
|
|
MoveLine = data.get('MoveLine')
|
|
|
|
tax_vals = [self._get_tax_vals(key, amounts['amount'], amounts['amount_converted'], amounts['base_amount_converted']) for key, amounts in taxes.items() if amounts['amount']]
|
|
# Check if all taxes lines have account_id assigned. If not, there are repartition lines of the tax that have no account_id.
|
|
tax_names_no_account = [line['name'] for line in tax_vals if line['account_id'] == False]
|
|
if len(tax_names_no_account) > 0:
|
|
error_message = _(
|
|
'Unable to close and validate the session.\n'
|
|
'Please set corresponding tax account in each repartition line of the following taxes: \n%s'
|
|
) % ', '.join(tax_names_no_account)
|
|
raise UserError(error_message)
|
|
rounding_vals = []
|
|
|
|
if not float_is_zero(rounding_difference['amount'], precision_rounding=self.currency_id.rounding) or not float_is_zero(rounding_difference['amount_converted'], precision_rounding=self.currency_id.rounding):
|
|
rounding_vals = [self._get_rounding_difference_vals(rounding_difference['amount'], rounding_difference['amount_converted'])]
|
|
|
|
MoveLine.create(
|
|
tax_vals
|
|
+ [self._get_sale_vals(key, amounts['amount'], amounts['amount_converted']) for key, amounts in sales.items()]
|
|
+ [self._get_stock_expense_vals(key, amounts['amount'], amounts['amount_converted']) for key, amounts in stock_expense.items()]
|
|
+ [self._get_split_receivable_vals(key, amounts['amount'], amounts['amount_converted']) for key, amounts in split_receivables.items()]
|
|
+ [self._get_combine_receivable_vals(key, amounts['amount'], amounts['amount_converted']) for key, amounts in combine_receivables.items()]
|
|
+ rounding_vals
|
|
)
|
|
return data
|
|
|
|
def _create_cash_statement_lines_and_cash_move_lines(self, data):
|
|
# Create the split and combine cash statement lines and account move lines.
|
|
# Keep the reference by statement for reconciliation.
|
|
# `split_cash_statement_lines` maps `statement` -> split cash statement lines
|
|
# `combine_cash_statement_lines` maps `statement` -> combine cash statement lines
|
|
# `split_cash_receivable_lines` maps `statement` -> split cash receivable lines
|
|
# `combine_cash_receivable_lines` maps `statement` -> combine cash receivable lines
|
|
MoveLine = data.get('MoveLine')
|
|
split_receivables_cash = data.get('split_receivables_cash')
|
|
combine_receivables_cash = data.get('combine_receivables_cash')
|
|
|
|
statements_by_journal_id = {statement.journal_id.id: statement for statement in self.statement_ids}
|
|
# handle split cash payments
|
|
split_cash_statement_line_vals = defaultdict(list)
|
|
split_cash_receivable_vals = defaultdict(list)
|
|
for payment, amounts in split_receivables_cash.items():
|
|
statement = statements_by_journal_id[payment.payment_method_id.cash_journal_id.id]
|
|
split_cash_statement_line_vals[statement].append(self._get_split_statement_line_vals(statement, payment, amounts['amount']))
|
|
split_cash_receivable_vals[statement].append(self._get_split_receivable_vals(payment, amounts['amount'], amounts['amount_converted']))
|
|
# handle combine cash payments
|
|
combine_cash_statement_line_vals = defaultdict(list)
|
|
combine_cash_receivable_vals = defaultdict(list)
|
|
for payment_method, amounts in combine_receivables_cash.items():
|
|
if not float_is_zero(amounts['amount'] , precision_rounding=self.currency_id.rounding):
|
|
statement = statements_by_journal_id[payment_method.cash_journal_id.id]
|
|
combine_cash_statement_line_vals[statement].append(self._get_statement_line_vals(statement, payment_method.receivable_account_id, amounts['amount']))
|
|
combine_cash_receivable_vals[statement].append(self._get_combine_receivable_vals(payment_method, amounts['amount'], amounts['amount_converted']))
|
|
# create the statement lines and account move lines
|
|
BankStatementLine = self.env['account.bank.statement.line']
|
|
split_cash_statement_lines = {}
|
|
combine_cash_statement_lines = {}
|
|
split_cash_receivable_lines = {}
|
|
combine_cash_receivable_lines = {}
|
|
for statement in self.statement_ids:
|
|
split_cash_statement_lines[statement] = BankStatementLine.create(split_cash_statement_line_vals[statement])
|
|
combine_cash_statement_lines[statement] = BankStatementLine.create(combine_cash_statement_line_vals[statement])
|
|
split_cash_receivable_lines[statement] = MoveLine.create(split_cash_receivable_vals[statement])
|
|
combine_cash_receivable_lines[statement] = MoveLine.create(combine_cash_receivable_vals[statement])
|
|
|
|
data.update(
|
|
{'split_cash_statement_lines': split_cash_statement_lines,
|
|
'combine_cash_statement_lines': combine_cash_statement_lines,
|
|
'split_cash_receivable_lines': split_cash_receivable_lines,
|
|
'combine_cash_receivable_lines': combine_cash_receivable_lines
|
|
})
|
|
return data
|
|
|
|
def _create_invoice_receivable_lines(self, data):
|
|
# Create invoice receivable lines for this session's move_id.
|
|
# Keep reference of the invoice receivable lines because
|
|
# they are reconciled with the lines in order_account_move_receivable_lines
|
|
MoveLine = data.get('MoveLine')
|
|
invoice_receivables = data.get('invoice_receivables')
|
|
|
|
invoice_receivable_vals = defaultdict(list)
|
|
invoice_receivable_lines = {}
|
|
for receivable_account_id, amounts in invoice_receivables.items():
|
|
invoice_receivable_vals[receivable_account_id].append(self._get_invoice_receivable_vals(receivable_account_id, amounts['amount'], amounts['amount_converted']))
|
|
for receivable_account_id, vals in invoice_receivable_vals.items():
|
|
receivable_line = MoveLine.create(vals)
|
|
if (not receivable_line.reconciled):
|
|
invoice_receivable_lines[receivable_account_id] = receivable_line
|
|
|
|
data.update({'invoice_receivable_lines': invoice_receivable_lines})
|
|
return data
|
|
|
|
def _create_stock_output_lines(self, data):
|
|
# Keep reference to the stock output lines because
|
|
# they are reconciled with output lines in the stock.move's account.move.line
|
|
MoveLine = data.get('MoveLine')
|
|
stock_output = data.get('stock_output')
|
|
stock_return = data.get('stock_return')
|
|
|
|
stock_output_vals = defaultdict(list)
|
|
stock_output_lines = {}
|
|
for stock_moves in [stock_output, stock_return]:
|
|
for account, amounts in stock_moves.items():
|
|
stock_output_vals[account].append(self._get_stock_output_vals(account, amounts['amount'], amounts['amount_converted']))
|
|
|
|
for output_account, vals in stock_output_vals.items():
|
|
stock_output_lines[output_account] = MoveLine.create(vals)
|
|
|
|
data.update({'stock_output_lines': stock_output_lines})
|
|
return data
|
|
|
|
def _reconcile_account_move_lines(self, data):
|
|
# reconcile cash receivable lines
|
|
split_cash_statement_lines = data.get('split_cash_statement_lines')
|
|
combine_cash_statement_lines = data.get('combine_cash_statement_lines')
|
|
split_cash_receivable_lines = data.get('split_cash_receivable_lines')
|
|
combine_cash_receivable_lines = data.get('combine_cash_receivable_lines')
|
|
order_account_move_receivable_lines = data.get('order_account_move_receivable_lines')
|
|
invoice_receivable_lines = data.get('invoice_receivable_lines')
|
|
stock_output_lines = data.get('stock_output_lines')
|
|
|
|
for statement in self.statement_ids:
|
|
if not self.config_id.cash_control:
|
|
statement.write({'balance_end_real': statement.balance_end})
|
|
statement.button_post()
|
|
all_lines = (
|
|
split_cash_statement_lines[statement].mapped('move_id.line_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
|
|
| combine_cash_statement_lines[statement].mapped('move_id.line_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
|
|
| split_cash_receivable_lines[statement]
|
|
| combine_cash_receivable_lines[statement]
|
|
)
|
|
accounts = all_lines.mapped('account_id')
|
|
lines_by_account = [all_lines.filtered(lambda l: l.account_id == account) for account in accounts]
|
|
for lines in lines_by_account:
|
|
lines.reconcile()
|
|
# We try to validate the statement after the reconciliation is done
|
|
# because validating the statement requires each statement line to be
|
|
# reconciled.
|
|
# Furthermore, if the validation failed, which is caused by unreconciled
|
|
# cash difference statement line, we just ignore that. Leaving the statement
|
|
# not yet validated. Manual reconciliation and validation should be made
|
|
# by the user in the accounting app.
|
|
try:
|
|
statement.button_validate()
|
|
except UserError:
|
|
pass
|
|
|
|
# reconcile invoice receivable lines
|
|
for account_id in order_account_move_receivable_lines:
|
|
( order_account_move_receivable_lines[account_id]
|
|
| invoice_receivable_lines.get(account_id, self.env['account.move.line'])
|
|
).reconcile()
|
|
|
|
# reconcile stock output lines
|
|
pickings = self.picking_ids.filtered(lambda p: not p.pos_order_id)
|
|
pickings |= self.order_ids.filtered(lambda o: not o.is_invoiced).mapped('picking_ids')
|
|
stock_moves = self.env['stock.move'].search([('picking_id', 'in', pickings.ids)])
|
|
stock_account_move_lines = self.env['account.move'].search([('stock_move_id', 'in', stock_moves.ids)]).mapped('line_ids')
|
|
for account_id in stock_output_lines:
|
|
( stock_output_lines[account_id]
|
|
| stock_account_move_lines.filtered(lambda aml: aml.account_id == account_id)
|
|
).filtered(lambda aml: not aml.reconciled).reconcile()
|
|
return data
|
|
|
|
def _prepare_line(self, order_line):
|
|
""" Derive from order_line the order date, income account, amount and taxes information.
|
|
|
|
These information will be used in accumulating the amounts for sales and tax lines.
|
|
"""
|
|
def get_income_account(order_line):
|
|
product = order_line.product_id
|
|
income_account = product.with_company(order_line.company_id)._get_product_accounts()['income']
|
|
if not income_account:
|
|
raise UserError(_('Please define income account for this product: "%s" (id:%d).')
|
|
% (product.name, product.id))
|
|
return order_line.order_id.fiscal_position_id.map_account(income_account)
|
|
|
|
tax_ids = order_line.tax_ids_after_fiscal_position\
|
|
.filtered(lambda t: t.company_id.id == order_line.order_id.company_id.id)
|
|
sign = -1 if order_line.qty >= 0 else 1
|
|
price = sign * order_line.price_unit * (1 - (order_line.discount or 0.0) / 100.0)
|
|
# The 'is_refund' parameter is used to compute the tax tags. Ultimately, the tags are part
|
|
# of the key used for summing taxes. Since the POS UI doesn't support the tags, inconsistencies
|
|
# may arise in 'Round Globally'.
|
|
check_refund = lambda x: x.qty * x.price_unit < 0
|
|
if self.company_id.tax_calculation_rounding_method == 'round_globally':
|
|
is_refund = all(check_refund(line) for line in order_line.order_id.lines)
|
|
else:
|
|
is_refund = check_refund(order_line)
|
|
tax_data = tax_ids.compute_all(price_unit=price, quantity=abs(order_line.qty), currency=self.currency_id, is_refund=is_refund)
|
|
taxes = tax_data['taxes']
|
|
# For Cash based taxes, use the account from the repartition line immediately as it has been paid already
|
|
for tax in taxes:
|
|
tax_rep = self.env['account.tax.repartition.line'].browse(tax['tax_repartition_line_id'])
|
|
tax['account_id'] = tax_rep.account_id.id
|
|
date_order = order_line.order_id.date_order
|
|
taxes = [{'date_order': date_order, **tax} for tax in taxes]
|
|
return {
|
|
'date_order': order_line.order_id.date_order,
|
|
'income_account_id': get_income_account(order_line).id,
|
|
'amount': order_line.price_subtotal,
|
|
'taxes': taxes,
|
|
'base_tags': tuple(tax_data['base_tags']),
|
|
}
|
|
|
|
def _get_rounding_difference_vals(self, amount, amount_converted):
|
|
if self.config_id.cash_rounding:
|
|
partial_args = {
|
|
'name': 'Rounding line',
|
|
'move_id': self.move_id.id,
|
|
}
|
|
if float_compare(0.0, amount, precision_rounding=self.currency_id.rounding) > 0: # loss
|
|
partial_args['account_id'] = self.config_id.rounding_method.loss_account_id.id
|
|
return self._debit_amounts(partial_args, -amount, -amount_converted)
|
|
|
|
if float_compare(0.0, amount, precision_rounding=self.currency_id.rounding) < 0: # profit
|
|
partial_args['account_id'] = self.config_id.rounding_method.profit_account_id.id
|
|
return self._credit_amounts(partial_args, amount, amount_converted)
|
|
|
|
def _get_split_receivable_vals(self, payment, amount, amount_converted):
|
|
partial_vals = {
|
|
'account_id': payment.payment_method_id.receivable_account_id.id,
|
|
'move_id': self.move_id.id,
|
|
'partner_id': self.env["res.partner"]._find_accounting_partner(payment.partner_id).id,
|
|
'name': '%s - %s' % (self.name, payment.payment_method_id.name),
|
|
}
|
|
return self._debit_amounts(partial_vals, amount, amount_converted)
|
|
|
|
def _get_combine_receivable_vals(self, payment_method, amount, amount_converted):
|
|
partial_vals = {
|
|
'account_id': payment_method.receivable_account_id.id,
|
|
'move_id': self.move_id.id,
|
|
'name': '%s - %s' % (self.name, payment_method.name)
|
|
}
|
|
return self._debit_amounts(partial_vals, amount, amount_converted)
|
|
|
|
def _get_invoice_receivable_vals(self, account_id, amount, amount_converted):
|
|
partial_vals = {
|
|
'account_id': account_id,
|
|
'move_id': self.move_id.id,
|
|
'name': 'From invoiced orders'
|
|
}
|
|
return self._credit_amounts(partial_vals, amount, amount_converted)
|
|
|
|
def _get_sale_vals(self, key, amount, amount_converted):
|
|
account_id, sign, tax_keys, base_tag_ids = key
|
|
tax_ids = set(tax[0] for tax in tax_keys)
|
|
applied_taxes = self.env['account.tax'].browse(tax_ids)
|
|
title = 'Sales' if sign == 1 else 'Refund'
|
|
name = '%s untaxed' % title
|
|
if applied_taxes:
|
|
name = '%s with %s' % (title, ', '.join([tax.name for tax in applied_taxes]))
|
|
partial_vals = {
|
|
'name': name,
|
|
'account_id': account_id,
|
|
'move_id': self.move_id.id,
|
|
'tax_ids': [(6, 0, tax_ids)],
|
|
'tax_tag_ids': [(6, 0, base_tag_ids)],
|
|
}
|
|
return self._credit_amounts(partial_vals, amount, amount_converted)
|
|
|
|
def _get_tax_vals(self, key, amount, amount_converted, base_amount_converted):
|
|
account_id, repartition_line_id, tax_id, tag_ids = key
|
|
tax = self.env['account.tax'].browse(tax_id)
|
|
partial_args = {
|
|
'name': tax.name,
|
|
'account_id': account_id,
|
|
'move_id': self.move_id.id,
|
|
'tax_base_amount': abs(base_amount_converted),
|
|
'tax_repartition_line_id': repartition_line_id,
|
|
'tax_tag_ids': [(6, 0, tag_ids)],
|
|
}
|
|
return self._debit_amounts(partial_args, amount, amount_converted)
|
|
|
|
def _get_stock_expense_vals(self, exp_account, amount, amount_converted):
|
|
partial_args = {'account_id': exp_account.id, 'move_id': self.move_id.id}
|
|
return self._debit_amounts(partial_args, amount, amount_converted, force_company_currency=True)
|
|
|
|
def _get_stock_output_vals(self, out_account, amount, amount_converted):
|
|
partial_args = {'account_id': out_account.id, 'move_id': self.move_id.id}
|
|
return self._credit_amounts(partial_args, amount, amount_converted, force_company_currency=True)
|
|
|
|
def _get_statement_line_vals(self, statement, receivable_account, amount, date=False):
|
|
return {
|
|
'date': fields.Date.context_today(self, timestamp=date),
|
|
'amount': amount,
|
|
'payment_ref': self.name,
|
|
'statement_id': statement.id,
|
|
'journal_id': statement.journal_id.id,
|
|
'counterpart_account_id': receivable_account.id,
|
|
}
|
|
|
|
def _get_split_statement_line_vals(self, statement, payment, amount):
|
|
res = self._get_statement_line_vals(statement, payment.payment_method_id.receivable_account_id, amount, payment.payment_date)
|
|
res['partner_id'] = self.env['res.partner']._find_accounting_partner(payment.partner_id).id
|
|
return res
|
|
|
|
def _update_amounts(self, old_amounts, amounts_to_add, date, round=True, force_company_currency=False):
|
|
"""Responsible for adding `amounts_to_add` to `old_amounts` considering the currency of the session.
|
|
|
|
old_amounts { new_amounts {
|
|
amount amounts_to_add { amount
|
|
amount_converted + amount -> amount_converted
|
|
[base_amount [base_amount] [base_amount
|
|
base_amount_converted] } base_amount_converted]
|
|
} }
|
|
|
|
NOTE:
|
|
- Notice that `amounts_to_add` does not have `amount_converted` field.
|
|
This function is responsible in calculating the `amount_converted` from the
|
|
`amount` of `amounts_to_add` which is used to update the values of `old_amounts`.
|
|
- Values of `amount` and/or `base_amount` should always be in session's currency [1].
|
|
- Value of `amount_converted` should be in company's currency
|
|
|
|
[1] Except when `force_company_currency` = True. It means that values in `amounts_to_add`
|
|
is in company currency.
|
|
|
|
:params old_amounts dict:
|
|
Amounts to update
|
|
:params amounts_to_add dict:
|
|
Amounts used to update the old_amounts
|
|
:params date date:
|
|
Date used for conversion
|
|
:params round bool:
|
|
Same as round parameter of `res.currency._convert`.
|
|
Defaults to True because that is the default of `res.currency._convert`.
|
|
We put it to False if we want to round globally.
|
|
:params force_company_currency bool:
|
|
If True, the values in amounts_to_add are in company's currency.
|
|
Defaults to False because it is only used to anglo-saxon lines.
|
|
|
|
:return dict: new amounts combining the values of `old_amounts` and `amounts_to_add`.
|
|
"""
|
|
# make a copy of the old amounts
|
|
new_amounts = { **old_amounts }
|
|
|
|
amount = amounts_to_add.get('amount')
|
|
if self.is_in_company_currency or force_company_currency:
|
|
amount_converted = amount
|
|
else:
|
|
amount_converted = self._amount_converter(amount, date, round)
|
|
|
|
# update amount and amount converted
|
|
new_amounts['amount'] += amount
|
|
new_amounts['amount_converted'] += amount_converted
|
|
|
|
# consider base_amount if present
|
|
if not amounts_to_add.get('base_amount') == None:
|
|
base_amount = amounts_to_add.get('base_amount')
|
|
if self.is_in_company_currency or force_company_currency:
|
|
base_amount_converted = base_amount
|
|
else:
|
|
base_amount_converted = self._amount_converter(base_amount, date, round)
|
|
|
|
# update base_amount and base_amount_converted
|
|
new_amounts['base_amount'] += base_amount
|
|
new_amounts['base_amount_converted'] += base_amount_converted
|
|
|
|
return new_amounts
|
|
|
|
def _round_amounts(self, amounts):
|
|
new_amounts = {}
|
|
for key, amount in amounts.items():
|
|
if key == 'amount_converted':
|
|
# round the amount_converted using the company currency.
|
|
new_amounts[key] = self.company_id.currency_id.round(amount)
|
|
else:
|
|
new_amounts[key] = self.currency_id.round(amount)
|
|
return new_amounts
|
|
|
|
def _credit_amounts(self, partial_move_line_vals, amount, amount_converted, force_company_currency=False):
|
|
""" `partial_move_line_vals` is completed by `credit`ing the given amounts.
|
|
|
|
NOTE Amounts in PoS are in the currency of journal_id in the session.config_id.
|
|
This means that amount fields in any pos record are actually equivalent to amount_currency
|
|
in account module. Understanding this basic is important in correctly assigning values for
|
|
'amount' and 'amount_currency' in the account.move.line record.
|
|
|
|
:param partial_move_line_vals dict:
|
|
initial values in creating account.move.line
|
|
:param amount float:
|
|
amount derived from pos.payment, pos.order, or pos.order.line records
|
|
:param amount_converted float:
|
|
converted value of `amount` from the given `session_currency` to company currency
|
|
|
|
:return dict: complete values for creating 'amount.move.line' record
|
|
"""
|
|
if self.is_in_company_currency or force_company_currency:
|
|
additional_field = {}
|
|
else:
|
|
additional_field = {
|
|
'amount_currency': -amount,
|
|
'currency_id': self.currency_id.id,
|
|
}
|
|
return {
|
|
'debit': -amount_converted if amount_converted < 0.0 else 0.0,
|
|
'credit': amount_converted if amount_converted > 0.0 else 0.0,
|
|
**partial_move_line_vals,
|
|
**additional_field,
|
|
}
|
|
|
|
def _debit_amounts(self, partial_move_line_vals, amount, amount_converted, force_company_currency=False):
|
|
""" `partial_move_line_vals` is completed by `debit`ing the given amounts.
|
|
|
|
See _credit_amounts docs for more details.
|
|
"""
|
|
if self.is_in_company_currency or force_company_currency:
|
|
additional_field = {}
|
|
else:
|
|
additional_field = {
|
|
'amount_currency': amount,
|
|
'currency_id': self.currency_id.id,
|
|
}
|
|
return {
|
|
'debit': amount_converted if amount_converted > 0.0 else 0.0,
|
|
'credit': -amount_converted if amount_converted < 0.0 else 0.0,
|
|
**partial_move_line_vals,
|
|
**additional_field,
|
|
}
|
|
|
|
def _amount_converter(self, amount, date, round):
|
|
# self should be single record as this method is only called in the subfunctions of self._validate_session
|
|
return self.currency_id._convert(amount, self.company_id.currency_id, self.company_id, date, round=round)
|
|
|
|
def show_journal_items(self):
|
|
self.ensure_one()
|
|
all_related_moves = self._get_related_account_moves()
|
|
return {
|
|
'name': _('Journal Items'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'account.move.line',
|
|
'view_mode': 'tree',
|
|
'view_id':self.env.ref('account.view_move_line_tree_grouped').id,
|
|
'domain': [('id', 'in', all_related_moves.mapped('line_ids').ids)],
|
|
'context': {
|
|
'journal_type':'general',
|
|
'search_default_group_by_move': 1,
|
|
'group_by':'move_id', 'search_default_posted':1,
|
|
'name_groupby':1,
|
|
},
|
|
}
|
|
|
|
def _get_related_account_moves(self):
|
|
def get_matched_move_lines(aml):
|
|
if aml.credit > 0:
|
|
return [r.debit_move_id.id for r in aml.matched_debit_ids]
|
|
else:
|
|
return [r.credit_move_id.id for r in aml.matched_credit_ids]
|
|
|
|
session_move = self.move_id
|
|
# get all the linked move lines to this account move.
|
|
non_reconcilable_lines = session_move.line_ids.filtered(lambda aml: not aml.account_id.reconcile)
|
|
reconcilable_lines = session_move.line_ids - non_reconcilable_lines
|
|
fully_reconciled_lines = reconcilable_lines.filtered(lambda aml: aml.full_reconcile_id)
|
|
partially_reconciled_lines = reconcilable_lines - fully_reconciled_lines
|
|
|
|
cash_move_lines = self.env['account.move.line'].search([('statement_id', '=', self.cash_register_id.id)])
|
|
|
|
ids = (non_reconcilable_lines.ids
|
|
+ fully_reconciled_lines.mapped('full_reconcile_id').mapped('reconciled_line_ids').ids
|
|
+ sum(partially_reconciled_lines.mapped(get_matched_move_lines), partially_reconciled_lines.ids)
|
|
+ cash_move_lines.ids)
|
|
|
|
return self.env['account.move.line'].browse(ids).mapped('move_id')
|
|
|
|
def action_show_payments_list(self):
|
|
return {
|
|
'name': _('Payments'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'pos.payment',
|
|
'view_mode': 'tree,form',
|
|
'domain': [('session_id', '=', self.id)],
|
|
'context': {'search_default_group_by_payment_method': 1}
|
|
}
|
|
|
|
def open_frontend_cb(self):
|
|
"""Open the pos interface with config_id as an extra argument.
|
|
|
|
In vanilla PoS each user can only have one active session, therefore it was not needed to pass the config_id
|
|
on opening a session. It is also possible to login to sessions created by other users.
|
|
|
|
:returns: dict
|
|
"""
|
|
if not self.ids:
|
|
return {}
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'target': 'self',
|
|
'url': self.config_id._get_pos_base_url() + '?config_id=%d' % self.config_id.id,
|
|
}
|
|
|
|
def open_cashbox_pos(self):
|
|
self.ensure_one()
|
|
action = self.cash_register_id.open_cashbox_id()
|
|
action['view_id'] = self.env.ref('point_of_sale.view_account_bnk_stmt_cashbox_footer').id
|
|
action['context']['pos_session_id'] = self.id
|
|
action['context']['default_pos_id'] = self.config_id.id
|
|
return action
|
|
|
|
def set_cashbox_pos(self, cashbox_value, notes):
|
|
self.state = 'opened'
|
|
self.cash_register_id.balance_start = cashbox_value
|
|
if notes:
|
|
self.env['mail.message'].create({
|
|
'body': notes,
|
|
'model': 'account.bank.statement',
|
|
'res_id': self.cash_register_id.id,
|
|
})
|
|
|
|
def action_view_order(self):
|
|
return {
|
|
'name': _('Orders'),
|
|
'res_model': 'pos.order',
|
|
'view_mode': 'tree,form',
|
|
'views': [
|
|
(self.env.ref('point_of_sale.view_pos_order_tree_no_session_id').id, 'tree'),
|
|
(self.env.ref('point_of_sale.view_pos_pos_form').id, 'form'),
|
|
],
|
|
'type': 'ir.actions.act_window',
|
|
'domain': [('session_id', 'in', self.ids)],
|
|
}
|
|
|
|
@api.model
|
|
def _alert_old_session(self):
|
|
# If the session is open for more then one week,
|
|
# log a next activity to close the session.
|
|
sessions = self.search([('start_at', '<=', (fields.datetime.now() - timedelta(days=7))), ('state', '!=', 'closed')])
|
|
for session in sessions:
|
|
if self.env['mail.activity'].search_count([('res_id', '=', session.id), ('res_model', '=', 'pos.session')]) == 0:
|
|
session.activity_schedule(
|
|
'point_of_sale.mail_activity_old_session',
|
|
user_id=session.user_id.id,
|
|
note=_(
|
|
"Your PoS Session is open since %(date)s, we advise you to close it and to create a new one.",
|
|
date=session.start_at,
|
|
)
|
|
)
|
|
|
|
def _warning_balance_closing(self):
|
|
self.ensure_one()
|
|
|
|
context = dict(self._context)
|
|
context['session_id'] = self.id
|
|
|
|
return {
|
|
'name': _('Balance control'),
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'closing.balance.confirm.wizard',
|
|
'views': [(False, 'form')],
|
|
'type': 'ir.actions.act_window',
|
|
'context': context,
|
|
'target': 'new'
|
|
}
|
|
|
|
def _check_if_no_draft_orders(self):
|
|
draft_orders = self.order_ids.filtered(lambda order: order.state == 'draft')
|
|
if draft_orders:
|
|
raise UserError(_(
|
|
'There are still orders in draft state in the session. '
|
|
'Pay or cancel the following orders to validate the session:\n%s'
|
|
) % ', '.join(draft_orders.mapped('name'))
|
|
)
|
|
return True
|
|
|
|
class ProcurementGroup(models.Model):
|
|
_inherit = 'procurement.group'
|
|
|
|
@api.model
|
|
def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False):
|
|
super(ProcurementGroup, self)._run_scheduler_tasks(use_new_cursor=use_new_cursor, company_id=company_id)
|
|
self.env['pos.session']._alert_old_session()
|
|
if use_new_cursor:
|
|
self.env.cr.commit()
|
|
|
|
class ClosingBalanceConfirm(models.TransientModel):
|
|
_name = 'closing.balance.confirm.wizard'
|
|
_description = 'This wizard is used to display a warning message if the manager wants to close a session with a too high difference between real and expected closing balance'
|
|
|
|
def confirm_closing_balance(self):
|
|
current_session = self.env['pos.session'].browse(self._context['session_id'])
|
|
return current_session._validate_session()
|