Skip to content

Instantly share code, notes, and snippets.

@jev-odoo
Last active December 9, 2020 11:04
Show Gist options
  • Select an option

  • Save jev-odoo/cfc902ec0e36d3f518d92aa96fc180cf to your computer and use it in GitHub Desktop.

Select an option

Save jev-odoo/cfc902ec0e36d3f518d92aa96fc180cf to your computer and use it in GitHub Desktop.
POS - Balance
# FOR VERSION 13
# Set sessions ids between the brackets, separated by a coma
# I.E. SESSION_ID = [12, 18, 132] or SESSION_ID = [4]
# Set DEFAULT_ACCOUNT account_id as simple integer (not required)
SESSION_IDS = []
MODE = 'check'
DEFAULT_ACCOUNT = 0
# DO NOT MODIFY
VALID_VERSIONS = (13,)
VALID_SESSION_STATES = ('opened', 'closing_control')
VALID_MODES = ('check', 'process')
LOG_LINE_ACTION_NAME = 'POS - Balance Session'
LOG_LINE_NAME = 'Support Intervention'
ON_SUCCESS_LOG_NOTE = 'This session was unbalanced and has been force closed by Odoo Support. An account_move_line has been posted to counter the difference.'
SAVE_LOGS = True # Save logs in ir.logging table accessible from technical menu
RAISE_LOGS = True # Show logs in a popup after the process
PARTIAL_LOGS = []
FULL_LOGS = []
INFO = 'Info'
WARNING = 'Warning'
ERROR = 'Error'
# -----===== HELPERS =====------
def _add_log_line(level, log_message, log_details: list = None):
if log_details:
details = []
for detail in log_details:
for key, value in detail.items():
if key == 'counter':
details.append('{}/{}'.format(value[0], value[1]))
else:
details.append('{}: {}'.format(key.title(), value))
log_message = '{} ({})'.format(log_message, ' - '.join(details))
PARTIAL_LOGS.append((log_message, level))
def check_mode():
if MODE not in VALID_MODES:
raise Warning('Invalid mode. (Actual mode: {}, Valid modes: {})'.format(MODE, iter_to_string(VALID_MODES)))
def check_version(env, versions: float = None):
latest_version = env['ir.module.module'].search([('name', '=', 'base')]).latest_version.replace('~', '-')
actual_version = 0
versions_to_check = [versions] if versions else VALID_VERSIONS
for version in versions_to_check:
if latest_version.startswith(str(version)) or latest_version.startswith('saas-%s' % str(version)):
actual_version = version
if not versions and not actual_version:
raise Warning('Invalid version. (Actual version: {} - Valid versions: {})'.format(latest_version, iter_to_string(VALID_VERSIONS)))
return actual_version
def commit(env, session):
if MODE == 'process':
commit_message = 'commiting'
post_note(env, session)
env.cr.commit()
else:
commit_message = 'rollbacking'
env.cr.rollback()
log_message = '{} mode detected, {} changes.'.format(MODE, commit_message)
log_details = [{'session_id': session.id}]
_add_log_line(INFO, log_message, log_details)
def format_logs(logs):
lines = ['{} - {}'.format(x[1], x[0]) for x in logs]
return '\n'.join(lines)
def get_session(env, additional_domains: list = None, raise_exception=True):
sessions = env['pos.session']
domain = []
if SESSION_IDS:
domain.append(('id', 'in', SESSION_IDS))
else:
if raise_exception:
raise Warning('Please define the session id.')
if additional_domains:
domain += additional_domains
sessions = sessions.search(domain, order='id ASC')
if not sessions:
raise Warning('No session found. ids: {}'.format(iter_to_string(SESSION_IDS)))
return sessions
def is_time_between(begin_time, end_time):
check_time = datetime.datetime.utcnow().time()
if begin_time < end_time:
return begin_time <= check_time <= end_time
else: # crosses midnight
return check_time >= begin_time or check_time <= end_time
def iter_to_string(iterable):
return ', '.join(str(x) for x in iterable)
def post_note(env, session):
if not check_version(env, 12.0) and ON_SUCCESS_LOG_NOTE:
message = '{}'.format(ON_SUCCESS_LOG_NOTE)
session.message_post(body=message)
def raise_logs():
if RAISE_LOGS:
lines = format_logs(FULL_LOGS)
raise Warning(lines)
def save_log_lines(env):
for line in PARTIAL_LOGS:
FULL_LOGS.append(line)
if SAVE_LOGS:
lines = format_logs(PARTIAL_LOGS)
data = ({
'create_date': datetime.datetime.now(),
'create_uid': env.uid,
'type': 'server',
'dbname': env.cr.dbname,
'name': LOG_LINE_NAME,
'level': 'info',
'message': lines,
'path': 'action',
'line': 0,
'func': LOG_LINE_ACTION_NAME,
})
env['ir.logging'].create(data)
env.cr.commit()
PARTIAL_LOGS.clear()
# -----===== SPECIFIC FUNCTIONS =====------
def _action_pos_session_closing_control(session):
session._check_pos_session_balance()
session.write({'state': 'closing_control', 'stop_at': datetime.datetime.now()})
def _get_account(env, session):
if DEFAULT_ACCOUNT:
account = env['account.account'].browse(DEFAULT_ACCOUNT).exists()
else:
account = session.company_id.account_default_pos_receivable_account_id or env['ir.property'].get('property_account_receivable_id', 'res.partner')
if not account:
raise Warning('Receivable Account not found.')
return account
def balance(env, session):
_action_pos_session_closing_control(session)
return _action_pos_session_close(env, session)
def _action_pos_session_close(env, session):
if session.cash_register_id and session.cash_control and abs(session.cash_register_difference) > session.config_id.amount_authorized_diff:
log_message = 'Session cash register is not balanced. Please balance before running this action. Skipping.'
log_details = [{'session_id': session.id}]
_add_log_line(ERROR, log_message, log_details)
return
return _validate_session(env, session)
def _create_account_move(env, session):
journal = session.config_id.journal_id
account_move = session.env['account.move'].with_context(default_journal_id=journal.id).create({
'journal_id': journal.id,
'date': datetime.datetime.now(),
'ref': session.name,
})
session.write({'move_id': account_move.id})
data = {}
data = session._accumulate_amounts(data)
data = session._create_non_reconciliable_move_lines(data)
data = session._create_cash_statement_lines_and_cash_move_lines(data)
data = session._create_invoice_receivable_lines(data)
data = session._create_stock_output_lines(data)
# The balance is done here
data = _create_extra_move_lines(env, session, data)
if not data:
return
data = session._reconcile_account_move_lines(data)
return True
def _create_extra_move_lines(env, session, data):
MoveLine = data.get('MoveLine')
extra_move_lines = _get_extra_move_lines_vals(env, session)
if not extra_move_lines:
return
MoveLine.create(extra_move_lines)
return data
def _get_extra_move_lines_vals(env, session):
res = session._get_extra_move_lines_vals()
debit = sum([amount.get('debit', 0) for amount in res]) or 0
credit = sum([amount.get('credit', 0) for amount in res]) or 0
rounding_difference = {'amount': 0.0, 'amount_converted': 0.0}
rounding_vals = []
rounding_difference['amount'] = sum(session.move_id.line_ids.mapped('debit')) - sum(session.move_id.line_ids.mapped('credit'))
rounding_difference['amount'] += (debit - credit)
rounding_difference['amount_converted'] = rounding_difference['amount']
if not session.company_id.currency_id.is_zero(rounding_difference['amount_converted']):
value = _get_rounding_difference_vals(env, session, rounding_difference['amount'], rounding_difference['amount_converted'])
if not value:
return
rounding_vals += [value]
amount = value['credit'] or value['debit']
log_message = 'Session balanced.'
log_details = [{'session_id': session.id},
{'amount': amount}]
_add_log_line(INFO, log_message, log_details)
return res + rounding_vals
def _get_rounding_difference_vals(env, session, amount, amount_converted):
partial_args = {
'name': 'Rounding error balancing line',
'move_id': session.move_id.id,
}
account = _get_account(env, session)
partial_args['account_id'] = account.id
if amount > 0: # loss
return session._credit_amounts(partial_args, amount, amount_converted)
else: # profit
return session._debit_amounts(partial_args, -amount, -amount_converted)
def _validate_session(env, session):
session.ensure_one()
session._check_if_no_draft_orders()
create_am = _create_account_move(env, session)
if not create_am:
return
if session.move_id.line_ids:
session.move_id.post()
# Set the uninvoiced orders' state to 'done'
session.env['pos.order'].search([('session_id', '=', session.id), ('state', '=', 'paid')]).write({'state': 'done'})
else:
# The cash register needs to be confirmed for cash diffs
# made thru cash in/out when sesion is in cash_control.
if session.config_id.cash_control:
session.cash_register_id.button_confirm_bank()
session.move_id.unlink()
session.write({'state': 'closed'})
log_message = 'Session successfully closed.'
log_details = [{'session_id': session.id}]
_add_log_line(INFO, log_message, log_details)
return True
# -----===== MAIN PROCESS =====------
def process(env):
check_mode()
check_version(env)
additional_domain = []
sessions = get_session(env, additional_domain)
for session in sessions:
if session.state not in VALID_SESSION_STATES:
log_message = 'Invalid session state, skipping.'
log_details = [{'session_id': session.id},
{'actual state': session.state},
{'valid states': iter_to_string(VALID_SESSION_STATES)}]
_add_log_line(WARNING, log_message, log_details)
continue
try:
if balance(env, session):
commit(env, session)
else:
env.cr.rollback()
except Exception as e:
log_message = 'Unexpected Error occured.'
log_details = [{'session_id': session.id},
{'error': str(e)}]
_add_log_line(ERROR, log_message, log_details)
env.cr.rollback()
save_log_lines(env)
raise_logs()
process(env)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment