Journal entries coming from PoS are based in UTC instead of local time. This commit is an improvement over 23d3856f2a4323097f51254b5e32c517d45bfe66 to add the timezone info opw-2371863 closes odoo/odoo#63075 X-original-commit: 30ee2ebae3275de259eff6019d3a4582ef86a729 Signed-off-by: Nicolas Lempereur (nle) <nle@odoo.com> Signed-off-by: agr-odoo <agr-odoo@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:
|
|
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:
|
|
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].filtered(lambda aml: not aml.reconciled)
|
|
| stock_account_move_lines.filtered(lambda aml: aml.account_id == account_id)
|
|
).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()
|