[IMP] point_of_sale: create one account move by session

Main goal of this task is to create single accounting entry
(AE) when closing a pos.session instead of individual AE for
each order in the session.

This significantly minimizes the number of created journal
entries by point_of_sale which also results to faster closing
of session when there is large numbers (order of thousands)
of orders. It also eases the reconciliation of payments because
on one hand, cash payments are automatically reconciled and on
the other, other payments are combine in a receivable line
based on the payment method.

**Important points to note**

1. Two new models (pos.payment.method and pos.payment) are
introduced replacing the functionalities of account.journal
(and account.bank.statement) and
account.bank.statement.line, which served before as payment
methods and payments.

2. The creation of single AE relies on the receivable_account_id
defined in each pos.payment.method. This receivable_account_id
is supposed to be different from the account module's receivable
account, thus, a new receivable account for pos is introduced
in each localization.

3. A simple example below can illustrate this change.

Given the following orders:

    +---------+----------+----------+-----+-------+-------+
    | order   | payments | product  | qty | price | total |
    +---------+----------+----------+-----+-------+-------+
    | order 1 | cash     | product1 |  10 |    10 |   100 |
    |         |          | product2 |   5 |    20 |   100 |
    +---------+----------+----------+-----+-------+-------+
    | order 2 | bank     | product2 |   7 |    20 |   140 |
    |         |          | product3 |   1 |    30 |    30 |
    +---------+----------+----------+-----+-------+-------+
    | order 3 | bank     | product1 |   1 |    10 |    10 |
    |         |          | product2 |   3 |    20 |    60 |
    |         |          | product3 |   5 |    30 |   150 |
    +---------+----------+----------+-----+-------+-------+

Instead of generating 3 accounting entries, a single
accounting entry (linked to the pos.session) will be created.
This accounting entry will have the following lines:

    +---------------------+---------+------------+
    | account             | balance | reconciled |
    +---------------------+---------+------------+
    | sale                |    -590 |    -       |
    | pos receivable cash |     200 |    yes     |
    | pos receivable bank |     390 |    no      |
    +---------------------+---------+------------+
    | Total balance       |     0.0 |            |
    +---------------------+---------+------------+

Note that the cash receivable line is already reconciled
because it can assumed that the payment is already received.
The unreconciled receivable line can be reconciled manually
using the reconciliation widget.

More examples can be seen in the tests.

TASK-ID: 1862388
This commit is contained in:
Joseph Caburnay
2019-08-09 13:57:11 +00:00
committed by Pierre Masereel
parent 536560bbf5
commit b234e97f87
32 changed files with 1045 additions and 885 deletions
@@ -9,7 +9,7 @@ class AccountBankStatement(models.Model):
_inherit = 'account.bank.statement'
def unlink(self):
for statement in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user):
for statement in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.pos_payment_method_ids):
raise UserError(_('You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations.') % (statement.name,))
return super(AccountBankStatement, self).unlink()
@@ -18,6 +18,6 @@ class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
def unlink(self):
for line in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user):
for line in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.pos_payment_method_ids):
raise UserError(_('You cannot modify anything on a bank statement line (name: %s) that was created by point of sale operations.') % (line.name,))
return super(AccountBankStatementLine, self).unlink()
+2 -1
View File
@@ -16,7 +16,6 @@
'data/digest_data.xml',
'wizard/pos_box.xml',
'wizard/pos_details.xml',
'wizard/pos_open_statement.xml',
'wizard/pos_payment.xml',
'views/pos_templates.xml',
'views/point_of_sale_template.xml',
@@ -26,6 +25,8 @@
'views/pos_category_view.xml',
'views/product_view.xml',
'views/account_journal_view.xml',
'views/pos_payment_method_views.xml',
'views/pos_payment_views.xml',
'views/pos_config_view.xml',
'views/pos_session_view.xml',
'views/point_of_sale_sequence.xml',
+2
View File
@@ -17,3 +17,5 @@ from . import res_partner
from . import res_company
from . import res_config_settings
from . import stock_warehouse
from . import pos_payment
from . import pos_payment_method
+1 -16
View File
@@ -7,19 +7,4 @@ from odoo import fields, models, api
class AccountJournal(models.Model):
_inherit = 'account.journal'
journal_user = fields.Boolean('Use in Point of Sale',
help="Check this box if this journal define a payment method that can be used in a point of sale.")
@api.model
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
session_id = self.env.context.get('pos_session_id', False)
if session_id:
session = self.env['pos.session'].browse(session_id)
if session:
args += [('id', 'in', session.config_id.journal_ids.ids)]
return super(AccountJournal, self)._search(args=args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
@api.onchange('type')
def onchange_type(self):
if self.type not in ['bank', 'cash']:
self.journal_user = False
pos_payment_method_ids = fields.One2many('pos.payment.method', 'cash_journal_id', string='Point of Sale Payment Methods')
+33 -18
View File
@@ -5,7 +5,7 @@ from datetime import datetime
from uuid import uuid4
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.exceptions import ValidationError, UserError
class AccountBankStmtCashWizard(models.Model):
@@ -81,9 +81,8 @@ class PosConfig(models.Model):
def _default_invoice_journal(self):
return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1)
def _get_default_journal_ids(self):
journals = self.env['account.journal'].search([('journal_user', '=', True), ('type', 'in', ['cash','bank']), ('company_id', '=', self.env.company.id)])
return journals or False
def _default_payment_methods(self):
return self.env['pos.payment.method'].search([('split_transactions', '=', False), ('company_id', '=', self.env.company.id)])
def _default_pricelist(self):
return self.env['product.pricelist'].search([('currency_id', '=', self.env.company.currency_id.id)], limit=1)
@@ -101,11 +100,6 @@ class PosConfig(models.Model):
name = fields.Char(string='Point of Sale', index=True, required=True, help="An internal identification of the point of sale.")
is_installed_account_accountant = fields.Boolean(string="Is the Full Accounting Installed",
compute="_compute_is_installed_account_accountant")
journal_ids = fields.Many2many(
'account.journal', 'pos_config_journal_rel',
'pos_config_id', 'journal_id', string='Available Payment Methods',
domain="[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash']), ('company_id', '=', company_id)]",
default=lambda self: self._get_default_journal_ids())
picking_type_id = fields.Many2one(
'stock.picking.type',
string='Operation Type',
@@ -169,8 +163,6 @@ class PosConfig(models.Model):
pos_session_username = fields.Char(compute='_compute_current_session_user')
pos_session_state = fields.Char(compute='_compute_current_session_user')
pos_session_duration = fields.Char(compute='_compute_current_session_user')
group_by = fields.Boolean(string='Group Journal Items', default=True,
help="Check this if you want to group the Journal Items by Product while closing a Session.")
pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist', required=True, default=_default_pricelist,
help="The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured.")
available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists', default=_default_pricelist,
@@ -208,6 +200,7 @@ class PosConfig(models.Model):
help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when "
"closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at "
"the closing of his session saying that he needs to contact his manager.")
payment_method_ids = fields.Many2many('pos.payment.method', string='Payment Methods', default=lambda self: self._default_payment_methods())
def _compute_is_installed_account_accountant(self):
account_accountant = self.env['ir.module.module'].sudo().search([('name', '=', 'account_accountant'), ('state', '=', 'installed')])
@@ -281,12 +274,12 @@ class PosConfig(models.Model):
if self.invoice_journal_id and self.invoice_journal_id.company_id.id != self.company_id.id:
raise ValidationError(_("The invoice journal and the point of sale must belong to the same company."))
@api.constrains('company_id', 'journal_ids')
@api.constrains('company_id', 'payment_method_ids')
def _check_company_payment(self):
if self.env['account.journal'].search_count([('id', 'in', self.journal_ids.ids), ('company_id', '!=', self.company_id.id)]):
if self.env['pos.payment.method'].search_count([('id', 'in', self.payment_method_ids.ids), ('company_id', '!=', self.company_id.id)]):
raise ValidationError(_("The method payments and the point of sale must belong to the same company."))
@api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'journal_ids')
@api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'payment_method_ids')
def _check_currencies(self):
if self.pricelist_id not in self.available_pricelist_ids:
raise ValidationError(_("The default pricelist must be included in the available pricelists."))
@@ -296,7 +289,11 @@ class PosConfig(models.Model):
" the Accounting application."))
if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id:
raise ValidationError(_("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set."))
if any(self.journal_ids.mapped(lambda journal: self.currency_id not in (journal.company_id.currency_id, journal.currency_id))):
if any(
self.payment_method_ids\
.filtered(lambda pm: pm.is_cash_count)\
.mapped(lambda pm: self.currency_id not in (self.company_id.currency_id | pm.cash_journal_id.currency_id))
):
raise ValidationError(_("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set."))
@api.constrains('company_id', 'available_pricelist_ids')
@@ -412,6 +409,9 @@ class PosConfig(models.Model):
if config_display:
super(PosConfig, config_display).write({'customer_facing_display_html': self._compute_default_customer_html()})
if self.current_session_id:
raise UserError(_('Unable to modify this PoS Configuration because there is an open PoS Session based on it.'))
self.sudo()._set_fiscal_position()
self.sudo()._check_modules_to_install()
self.sudo()._check_groups_implied()
@@ -526,9 +526,24 @@ class PosConfig(models.Model):
for company in companies:
if company.chart_template_id:
cash_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('type', '=', 'cash')], limit=1)
cash_journal.write({'journal_user':True})
existing_pos_config = self.env['pos.config'].search([('company_id', '=', company.id), ('journal_ids', '=', False)])
existing_pos_config.write({'journal_ids': [(6, 0, cash_journal.ids)]})
pos_receivable_account = company.account_default_pos_receivable_account_id
payment_methods = self.env['pos.payment.method']
if cash_journal:
payment_methods |= payment_methods.create({
'name': _('Cash'),
'receivable_account_id': pos_receivable_account.id,
'is_cash_count': True,
'cash_journal_id': cash_journal.id,
'company_id': company.id,
})
payment_methods |= payment_methods.create({
'name': _('Bank'),
'receivable_account_id': pos_receivable_account.id,
'is_cash_count': False,
'company_id': company.id,
})
existing_pos_config = self.env['pos.config'].search([('company_id', '=', company.id), ('payment_method_ids', '=', False)])
existing_pos_config.write({'payment_method_ids': [(6, 0, payment_methods.ids)]})
@api.model
def generate_pos_journal(self, companies=False):
+44 -452
View File
@@ -47,17 +47,18 @@ class PosOrder(models.Model):
'amount_total': ui_order['amount_total'],
'amount_tax': ui_order['amount_tax'],
'amount_return': ui_order['amount_return'],
'company_id': self.env['pos.session'].browse(ui_order['pos_session_id']).company_id.id,
}
def _payment_fields(self, ui_paymentline):
@api.model
def _payment_fields(self, order, ui_paymentline):
payment_date = ui_paymentline['name']
payment_date = fields.Date.context_today(self, fields.Datetime.from_string(payment_date))
return {
'amount': ui_paymentline['amount'] or 0.0,
'amount': ui_paymentline['amount'] or 0.0,
'payment_date': payment_date,
'statement_id': ui_paymentline['statement_id'],
'payment_name': ui_paymentline.get('note', False),
'journal': ui_paymentline['journal_id'],
'payment_method_id': ui_paymentline['payment_method_id'],
'pos_order_id': order.id,
}
# This deals with orders that belong to a closed session. In order
@@ -94,24 +95,6 @@ class PosOrder(models.Model):
return new_session
def _match_payment_to_invoice(self, order):
pricelist_id = self.env['product.pricelist'].browse(order.get('pricelist_id'))
account_precision = pricelist_id.currency_id.decimal_places
# ignore orders with an amount_paid of 0 because those are returns through the POS
if not float_is_zero(order['amount_return'], account_precision) and not float_is_zero(order['amount_paid'], account_precision):
cur_amount_paid = 0
payments_to_keep = []
for payment in order.get('statement_ids'):
if cur_amount_paid + payment[2]['amount'] > order['amount_total']:
payment[2]['amount'] = order['amount_total'] - cur_amount_paid
payments_to_keep.append(payment)
break
cur_amount_paid += payment[2]['amount']
payments_to_keep.append(payment)
order['statement_ids'] = payments_to_keep
order['amount_return'] = 0
@api.model
def _process_order(self, pos_order):
pos_session = self.env['pos.session'].browse(pos_order['pos_session_id'])
@@ -119,86 +102,28 @@ class PosOrder(models.Model):
pos_order['pos_session_id'] = self._get_valid_session(pos_order).id
order = self.create(self._order_fields(pos_order))
prec_acc = order.pricelist_id.currency_id.decimal_places
journal_ids = set()
for payments in pos_order['statement_ids']:
if not float_is_zero(payments[2]['amount'], precision_digits=prec_acc):
order.add_payment(self._payment_fields(payments[2]))
journal_ids.add(payments[2]['journal_id'])
order.add_payment(self._payment_fields(order, payments[2]))
if pos_session.sequence_number <= pos_order['sequence_number']:
pos_session.write({'sequence_number': pos_order['sequence_number'] + 1})
pos_session.refresh()
if not float_is_zero(pos_order['amount_return'], prec_acc):
cash_journal_id = pos_session.cash_journal_id.id
if not cash_journal_id:
# Select for change one of the cash journals used in this
# payment
cash_journal = self.env['account.journal'].search([
('type', '=', 'cash'),
('id', 'in', list(journal_ids)),
], limit=1)
if not cash_journal:
# If none, select for change one of the cash journals of the POS
# This is used for example when a customer pays by credit card
# an amount higher than total amount of the order and gets cash back
cash_journal = [statement.journal_id for statement in pos_session.statement_ids if statement.journal_id.type == 'cash']
if not cash_journal:
raise UserError(_("No cash statement found for this session. Unable to record returned cash."))
cash_journal_id = cash_journal[0].id
order.add_payment({
cash_payment_method = pos_session.payment_method_ids.filtered('is_cash_count')[:1]
if not cash_payment_method:
raise UserError(_("No cash statement found for this session. Unable to record returned cash."))
return_payment_vals = {
'name': _('return'),
'pos_order_id': order.id,
'amount': -pos_order['amount_return'],
'payment_date': fields.Date.context_today(self),
'payment_name': _('return'),
'journal': cash_journal_id,
})
'payment_method_id': cash_payment_method.id,
}
order.add_payment(return_payment_vals)
return order
def _create_account_move(self):
self.ensure_one()
date_tz_user = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.session_id.start_at))
date_tz_user = fields.Date.to_string(date_tz_user)
return self.env['account.move'].with_context(default_journal_id=self.sale_journal.id).sudo().create({
'ref': self.name,
'journal_id': self.sale_journal.id,
'date': date_tz_user
})
@api.model
def _get_account_move_line_group_data_type_key(self, data_type, values, options={}):
"""
Return a tuple which will be used as a key for grouping account
move lines in _create_account_move_line method.
:param data_type: 'product', 'tax', ....
:param values: account move line values
:return: tuple() representing the data_type key
"""
if data_type == 'product':
return ('product',
values['partner_id'],
(values['product_id'], tuple(values['tax_ids'][0][2]), values['name']),
values['debit'] > 0,
values.get('currency_id'))
elif data_type == 'tax':
order_id = values.pop('order_id', False)
tax_key = ('tax',
values['partner_id'],
values['tax_line_id'],
values['debit'] > 0,
values.get('currency_id'))
if options.get('rounding_method') == 'round_globally':
tax_key = ('tax',
values['tax_line_id'],
order_id)
return tax_key
elif data_type == 'counter_part':
return ('counter_part',
values['partner_id'],
values['account_id'],
values['debit'] > 0,
values.get('currency_id'))
return False
def _prepare_invoice_line(self, order_line):
return {
'product_id': order_line.product_id.id,
@@ -209,242 +134,6 @@ class PosOrder(models.Model):
'tax_ids': [(6, 0, order_line.tax_ids.ids)],
}
def _prepare_account_move_line(self, line, partner_id, current_company, currency_id, rounding_method):
res = []
order = line.order_id
cur_company = order.company_id.currency_id
date_order = order.date_order.date() if order.date_order else fields.Date.today()
if currency_id != cur_company:
amount_subtotal = currency_id._convert(line.price_subtotal, cur_company, order.company_id, date_order)
else:
amount_subtotal = line.price_subtotal
# Search for the income account
if line.product_id.property_account_income_id.id:
income_account = line.product_id.property_account_income_id
elif line.product_id.categ_id.property_account_income_categ_id.id:
income_account = line.product_id.categ_id.property_account_income_categ_id
else:
raise UserError(_('Please define income '
'account for this product: "%s" (id:%d).')
% (line.product_id.name, line.product_id.id))
fpos = order.fiscal_position_id or order.partner_id.property_account_position_id
if fpos:
income_account = fpos.map_account(income_account)
name = line.product_id.name
if line.notice:
# add discount reason in move
name = name + ' (' + line.notice + ')'
# Create a move for the line for the order line
# Just like for invoices, a group of taxes must be present on this base line
# As well as its children
base_line_tax_ids = line.tax_ids_after_fiscal_position.flatten_taxes_hierarchy().filtered(lambda tax: tax.type_tax_use in ['sale', 'none'])
base_line_taxes_repartition = base_line_tax_ids.mapped(line.qty<0 and 'refund_repartition_line_ids' or 'invoice_repartition_line_ids')
base_line_tags = base_line_taxes_repartition.filtered(lambda x: x.repartition_type == 'base').mapped('tag_ids')
data = {
'name': name,
'quantity': line.qty,
'product_id': line.product_id.id,
'account_id': income_account.id,
'credit': ((amount_subtotal > 0) and amount_subtotal) or 0.0,
'debit': ((amount_subtotal < 0) and -amount_subtotal) or 0.0,
'tax_ids': [(6, 0, base_line_tax_ids.ids)],
'partner_id': partner_id,
'tag_ids': [(6, 0, base_line_tags.ids)],
}
if currency_id != cur_company:
data['currency_id'] = currency_id.id
data['amount_currency'] = -abs(line.price_subtotal) if data.get('credit') else abs(line.price_subtotal)
res.append({'data_type': 'product', 'values': data})
# Create the tax lines
taxes = line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == current_company.id)
if taxes:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
for tax in taxes.compute_all(price, currency_id, line.qty, is_refund=line.qty < 0)['taxes']:
if currency_id != cur_company:
round_tax = False if rounding_method == 'round_globally' else True
amount_tax = currency_id._convert(tax['amount'], cur_company, order.company_id, date_order, round=round_tax)
# amount_tax = currency_id.with_context(date=date_order).compute(tax['amount'], cur_company, round=round_tax)
else:
amount_tax = tax['amount']
data = {
'name': _('Tax') + ' ' + tax['name'],
'product_id': line.product_id.id,
'quantity': line.qty,
'account_id': tax['account_id'] or income_account.id,
'credit': ((amount_tax > 0) and amount_tax) or 0.0,
'debit': ((amount_tax < 0) and -amount_tax) or 0.0,
'tax_line_id': tax['id'],
'partner_id': partner_id,
'order_id': order.id,
'tax_repartition_line_id': tax['tax_repartition_line_id'],
'tax_base_amount': tax['base'],
'tag_ids': tax['tag_ids'],
}
if currency_id != cur_company:
data['currency_id'] = currency_id.id
data['amount_currency'] = -abs(tax['amount']) if data.get('credit') else abs(tax['amount'])
res.append({'data_type': 'tax', 'values': data})
return res
def _create_account_move_line(self, session=None):
# Tricky, via the workflow, we only have one id in the ids variable
"""Create a account move line of order grouped by products or not."""
IrProperty = self.env['ir.property']
ResPartner = self.env['res.partner']
if session and not all(session.id == order.session_id.id for order in self):
raise UserError(_('Selected orders do not have the same session!'))
grouped_data = {}
have_to_group_by = session and session.config_id.group_by or False
rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method
def add_anglosaxon_lines(grouped_data):
Product = self.env['product.product']
Analytic = self.env['account.analytic.account']
for product_key in list(grouped_data.keys()):
if product_key[0] == "product":
line = grouped_data[product_key][0]
product = Product.browse(line['product_id'])
# In the SO part, the entries will be inverted by function compute_invoice_totals
price_unit = self._get_pos_anglo_saxon_price_unit(product, line['partner_id'], line['quantity'])
account_analytic = Analytic.browse(line.get('analytic_account_id'))
res = Product._anglo_saxon_sale_move_lines(
line['name'], product, product.uom_id, line['quantity'], price_unit,
fiscal_position=order.fiscal_position_id,
account_analytic=account_analytic)
if res:
line1, line2 = res
insert_data('counter_part', {
'name': line1['name'],
'account_id': line1['account_id'],
'credit': line1['price'] < 0 and -line1['price'],
'debit': line1['price'] > 0 and line1['price'],
'partner_id': order.partner_id.id
})
insert_data('counter_part', {
'name': line2['name'],
'account_id': line2['account_id'],
'credit': line2['price'] < 0 and -line2['price'],
'debit': line2['price'] > 0 and line2['price'],
'partner_id': order.partner_id.id
})
move = False
for order in self.filtered(lambda o: not o.account_move or o.state == 'paid'):
current_company = order.sale_journal.company_id
account_def = IrProperty.get(
'property_account_receivable_id', 'res.partner')
order_account = order.partner_id.property_account_receivable_id or account_def
# If fiscal position, then map order account
fpos = order.fiscal_position_id or order.partner_id.property_account_position_id
if fpos:
order_account = fpos.map_account(order_account)
partner_id = ResPartner._find_accounting_partner(order.partner_id).id or False
if not move:
# Create an entry for the sale
move = order._create_account_move()
def insert_data(data_type, values):
# if have_to_group_by:
values.update({
'move_id': move.id,
})
key = self._get_account_move_line_group_data_type_key(data_type, values, {'rounding_method': rounding_method})
if not key:
return
grouped_data.setdefault(key, [])
if have_to_group_by:
if not grouped_data[key]:
grouped_data[key].append(values)
else:
current_value = grouped_data[key][0]
current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0)
current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
if 'currency_id' in values:
current_value['amount_currency'] = current_value.get('amount_currency', 0.0) + values.get('amount_currency', 0.0)
if key[0] == 'tax' and rounding_method == 'round_globally':
if current_value['debit'] - current_value['credit'] > 0:
current_value['debit'] = current_value['debit'] - current_value['credit']
current_value['credit'] = 0
else:
current_value['credit'] = current_value['credit'] - current_value['debit']
current_value['debit'] = 0
else:
grouped_data[key].append(values)
# because of the weird way the pos order is written, we need to make sure there is at least one line,
# because just after the 'for' loop there are references to 'line' and 'income_account' variables (that
# are set inside the for loop)
# TOFIX: a deep refactoring of this method (and class!) is needed
# in order to get rid of this stupid hack
assert order.lines, _('The POS order must have lines when calling this method')
# Create an move for each order line
cur = order.pricelist_id.currency_id
cur_company = order.company_id.currency_id
amount_cur_company = 0.0
for line in order.lines:
for move_line in self._prepare_account_move_line(line, partner_id, current_company, cur, rounding_method):
if cur != cur_company:
amount_cur_company += move_line['values']['credit'] - move_line['values']['debit']
insert_data(move_line['data_type'], move_line['values'])
# round tax lines per order
if rounding_method == 'round_globally':
for group_key, group_value in grouped_data.items():
if group_key[0] == 'tax':
for line in group_value:
line['credit'] = cur_company.round(line['credit'])
line['debit'] = cur_company.round(line['debit'])
if line.get('currency_id'):
line['amount_currency'] = cur.round(line.get('amount_currency', 0.0))
# counterpart
if cur != cur_company:
# 'amount_cur_company' contains the sum of the AML converted in the company
# currency. This makes the logic consistent with 'compute_invoice_totals' from
# 'account.move'. It ensures that the counterpart line is the same amount than
# the sum of the product and taxes lines.
amount_total = amount_cur_company
else:
amount_total = order.amount_total
data = {
'name': _("Trade Receivables"), # order.name,
'account_id': order_account.id,
'credit': ((amount_total < 0) and -amount_total) or 0.0,
'debit': ((amount_total > 0) and amount_total) or 0.0,
'partner_id': partner_id
}
if cur != cur_company:
data['currency_id'] = cur.id
data['amount_currency'] = -abs(order.amount_total) if data.get('credit') else abs(order.amount_total)
insert_data('counter_part', data)
order.write({'state': 'done', 'account_move': move.id})
if self and order.company_id.anglo_saxon_accounting:
add_anglosaxon_lines(grouped_data)
all_lines = []
for group_key, group_data in grouped_data.items():
for value in group_data:
all_lines.append((0, 0, value),)
if move: # In case no order was changed
move.sudo().write({'line_ids': all_lines})
move.sudo().post()
return True
def _get_pos_anglo_saxon_price_unit(self, product, partner_id, quantity):
moves = self.filtered(lambda o: o.partner_id.id == partner_id)\
.mapped('picking_id.move_lines')\
@@ -453,31 +142,6 @@ class PosOrder(models.Model):
price_unit = product._compute_average_price(0, quantity, moves)
return - price_unit
def _reconcile_payments(self):
for order in self:
aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids
aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id)
try:
# Cash returns will be well reconciled
# Whereas freight returns won't be
# "c'est la vie..."
aml.reconcile()
except Exception:
# There might be unexpected situations where the automatic reconciliation won't
# work. We don't want the user to be blocked because of this, since the automatic
# reconciliation is introduced for convenience, not for mandatory accounting
# reasons.
# It may be interesting to have the Traceback logged anyway
# for debugging and support purposes
_logger.exception('Reconciliation did not work for order %s', order.name)
def _filtered_for_reconciliation(self):
filter_states = ['invoiced', 'done']
if self.env['ir.config_parameter'].sudo().get_param('point_of_sale.order_reconcile_mode', 'all') == 'partner_only':
return self.filtered(lambda order: order.state in filter_states and order.partner_id)
return self.filtered(lambda order: order.state in filter_states)
name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/')
date_order = fields.Datetime(string='Date', readonly=True, index=True, default=fields.Datetime.now)
user_id = fields.Many2one(
@@ -492,7 +156,6 @@ class PosOrder(models.Model):
readonly=True, digits=0, required=True)
amount_return = fields.Float(string='Returned', digits=0, required=True, readonly=True)
lines = fields.One2many('pos.order.line', 'order_id', string='Order Lines', states={'draft': [('readonly', False)]}, readonly=True, copy=True)
statement_ids = fields.One2many('account.bank.statement.line', 'pos_statement_id', string='Payments', states={'draft': [('readonly', False)]}, readonly=True)
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True)
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, states={
'draft': [('readonly', False)]}, readonly=True)
@@ -512,7 +175,7 @@ class PosOrder(models.Model):
[('draft', 'New'), ('cancel', 'Cancelled'), ('paid', 'Paid'), ('done', 'Posted'), ('invoiced', 'Invoiced')],
'Status', readonly=True, copy=False, default='draft')
account_move = fields.Many2one('account.move', string='Journal Entry', readonly=True, copy=False)
account_move = fields.Many2one('account.move', string='Invoice', readonly=True, copy=False)
picking_id = fields.Many2one('stock.picking', string='Picking', readonly=True, copy=False)
picking_type_id = fields.Many2one('stock.picking.type', related='session_id.config_id.picking_type_id', string="Operation Type", readonly=False)
location_id = fields.Many2one(
@@ -530,6 +193,19 @@ class PosOrder(models.Model):
readonly=True,
states={'draft': [('readonly', False)]},
)
payment_ids = fields.One2many('pos.payment', 'pos_order_id', string='Payments', readonly=True)
session_move_id = fields.Many2one('account.move', string='Session Journal Entry', related='session_id.move_id', readonly=True, copy=False)
is_invoiced = fields.Boolean('Is Invoiced', compute='_compute_is_invoiced')
@api.depends('payment_ids', 'payment_ids.amount')
def _compute_amount_paid(self):
for order in self:
order.amount_paid = sum(order.payment_ids.mapped('amount'))
@api.depends('account_move')
def _compute_is_invoiced(self):
for order in self:
order.is_invoiced = bool(order.account_move)
@api.depends('date_order', 'company_id', 'currency_id', 'company_id.currency_id')
def _compute_currency_rate(self):
@@ -537,12 +213,12 @@ class PosOrder(models.Model):
order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order)
@api.onchange('statement_ids', 'lines')
@api.onchange('payment_ids', 'lines')
def _onchange_amount_all(self):
for order in self:
currency = order.pricelist_id.currency_id
order.amount_paid = sum(payment.amount for payment in order.statement_ids)
order.amount_return = sum(payment.amount < 0 and payment.amount or 0 for payment in order.statement_ids)
order.amount_paid = sum(payment.amount for payment in order.payment_ids)
order.amount_return = sum(payment.amount < 0 and payment.amount or 0 for payment in order.payment_ids)
order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines))
amount_untaxed = currency.round(sum(line.price_subtotal for line in order.lines))
order.amount_total = order.amount_tax + amount_untaxed
@@ -554,10 +230,10 @@ class PosOrder(models.Model):
Practical to be used for migrations
"""
amounts = {order_id: {'paid': 0, 'return': 0, 'taxed': 0, 'taxes': 0} for order_id in self.ids}
for order in self.env['account.bank.statement.line'].read_group([('pos_statement_id', 'in', self.ids)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
amounts[order['pos_statement_id'][0]]['paid'] = order['amount']
for order in self.env['account.bank.statement.line'].read_group(['&', ('pos_statement_id', 'in', self.ids), ('amount', '<', 0)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
amounts[order['pos_statement_id'][0]]['return'] = order['amount']
for order in self.env['pos.payment'].read_group([('pos_order_id', 'in', self.ids)], ['pos_order_id', 'amount'], ['pos_order_id']):
amounts[order['pos_order_id'][0]]['paid'] = order['amount']
for order in self.env['pos.payment'].read_group(['&', ('pos_order_id', 'in', self.ids), ('amount', '<', 0)], ['pos_order_id', 'amount'], ['pos_order_id']):
amounts[order['pos_order_id'][0]]['return'] = order['amount']
for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal', 'price_subtotal_incl'], ['order_id']):
amounts[order['order_id'][0]]['taxed'] = order['price_subtotal_incl']
amounts[order['order_id'][0]]['taxes'] = order['price_subtotal_incl'] - order['price_subtotal']
@@ -574,22 +250,7 @@ class PosOrder(models.Model):
@api.onchange('partner_id')
def _onchange_partner_id(self):
if self.partner_id:
self.pricelist = self.partner_id.property_product_pricelist.id
def write(self, vals):
res = super(PosOrder, self).write(vals)
Partner = self.env['res.partner']
# If you change the partner of the PoS order, change also the partner of the associated bank statement lines
if 'partner_id' in vals:
for order in self:
partner_id = False
if order.account_move:
raise UserError(_("You cannot change the partner of a POS order for which an invoice has already been issued."))
if vals['partner_id']:
partner = Partner.browse(vals['partner_id'])
partner_id = Partner._find_accounting_partner(partner).id
order.statement_ids.write({'partner_id': partner_id})
return res
self.pricelist_id = self.partner_id.property_product_pricelist.id
def unlink(self):
for pos_order in self.filtered(lambda pos_order: pos_order.state not in ['draft', 'cancel']):
@@ -622,8 +283,8 @@ class PosOrder(models.Model):
}
def action_pos_order_paid(self):
if not self.test_paid():
raise UserError(_("Order is not paid."))
if not float_is_zero(self.amount_total - self.amount_paid, precision_rounding=self.currency_id.rounding):
raise UserError(_("Order %s is not fully paid.") % self.name)
self.write({'state': 'paid'})
return self.create_picking()
@@ -681,9 +342,6 @@ class PosOrder(models.Model):
def action_pos_order_cancel(self):
return self.write({'state': 'cancel'})
def action_pos_order_done(self):
return self._create_account_move_line()
@api.model
def create_from_ui(self, orders):
# Keep only new orders
@@ -697,8 +355,7 @@ class PosOrder(models.Model):
for tmp_order in orders_to_save:
to_invoice = tmp_order['to_invoice']
order = tmp_order['data']
if to_invoice:
self._match_payment_to_invoice(order)
pos_order = self._process_order(order)
order_ids.append(pos_order.id)
@@ -715,17 +372,6 @@ class PosOrder(models.Model):
pos_order.account_move.sudo().with_context(force_company=self.env.user.company_id.id).post()
return order_ids
def test_paid(self):
"""A Point of Sale is paid when the sum
@return: True
"""
for order in self:
if order.lines and not order.amount_total:
continue
if (not order.lines) or (not order.statement_ids) or (abs(order.amount_total - order.amount_paid) > 0.00001):
return False
return True
def create_picking(self):
"""Create a picking for each order and validate it."""
Picking = self.env['stock.picking']
@@ -866,66 +512,11 @@ class PosOrder(models.Model):
move._set_quantity_done(qty_done)
return has_wrong_lots
def _prepare_bank_statement_line_payment_values(self, data):
"""Create a new payment for the order"""
payment_name = '%s - %s' % (self.name, self.pos_reference)
if data.get('payment_name'):
payment_name += ': ' + data['payment_name']
args = {
'amount': data['amount'],
'date': data.get('payment_date', fields.Date.context_today(self)),
'name': payment_name,
'partner_id': self.env["res.partner"]._find_accounting_partner(self.partner_id).id or False,
}
journal_id = data.get('journal', False)
statement_id = data.get('statement_id', False)
assert journal_id or statement_id, "No statement_id or journal_id passed to the method!"
journal = self.env['account.journal'].browse(journal_id)
# use the company of the journal and not of the current user
company_cxt = dict(self.env.context, force_company=journal.company_id.id)
account_def = self.env['ir.property'].with_context(company_cxt).get('property_account_receivable_id', 'res.partner')
args['account_id'] = (self.partner_id.property_account_receivable_id.id) or (account_def and account_def.id) or False
if not args['account_id']:
if not args['partner_id']:
msg = _('There is no receivable account defined to make payment.')
else:
msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (
self.partner_id.name, self.partner_id.id,)
raise UserError(msg)
context = dict(self.env.context)
context.pop('pos_session_id', False)
for statement in self.session_id.statement_ids:
if statement.id == statement_id:
journal_id = statement.journal_id.id
break
elif statement.journal_id.id == journal_id:
statement_id = statement.id
break
if not statement_id:
raise UserError(_('You have to open at least one cashbox.'))
args.update({
'statement_id': statement_id,
'pos_statement_id': self.id,
'journal_id': journal_id,
'ref': self.session_id.name,
})
return args
def add_payment(self, data):
"""Create a new payment for the order"""
self.ensure_one()
args = self._prepare_bank_statement_line_payment_values(data)
context = dict(self.env.context)
context.pop('pos_session_id', False)
self.env['account.bank.statement.line'].with_context(context).create(args)
self.amount_paid = sum(payment.amount for payment in self.statement_ids)
return args.get('statement_id', False)
self.env['pos.payment'].create(data)
self.amount_paid = sum(payment.amount for payment in self.payment_ids)
def refund(self):
"""Create a copy of order for refund order"""
@@ -1004,6 +595,7 @@ class PosOrderLine(models.Model):
tax_ids_after_fiscal_position = fields.Many2many('account.tax', compute='_get_tax_ids_after_fiscal_position', string='Taxes to Apply')
pack_lot_ids = fields.One2many('pos.pack.operation.lot', 'pos_order_line_id', string='Lot/serial Number')
product_uom_id = fields.Many2one('uom.uom', string='Product UoM', related='product_id.uom_id')
currency_id = fields.Many2one('res.currency', related='order_id.currency_id')
@api.model
def _prepare_refund_data(self, refund_order_id):
@@ -0,0 +1,25 @@
from odoo import api, fields, models
class PosPayment(models.Model):
""" Used to register payments made in a pos.order.
See `payment_ids` field of pos.order model.
The main characteristics of pos.payment can be read from
`payment_method_id`.
"""
_name = "pos.payment"
_description = "Point of Sale Payments"
_order = "id desc"
name = fields.Char(string='Label', readonly=True)
pos_order_id = fields.Many2one('pos.order', string='Order', required=True)
amount = fields.Monetary(string='Amount', required=True, currency_field='currency_id', readonly=True, help="Total amount of the payment.")
payment_method_id = fields.Many2one('pos.payment.method', string='Payment Method', required=True)
payment_date = fields.Datetime(string='Date', required=True, readonly=True, default=lambda self: fields.Datetime.now())
currency_id = fields.Many2one('res.currency', string='Currency', related='pos_order_id.currency_id')
currency_rate = fields.Float(string='Conversion Rate', related='pos_order_id.currency_rate', help='Conversion rate from company currency to order currency.')
partner_id = fields.Many2one('res.partner', string='Customer', related='pos_order_id.partner_id')
session_id = fields.Many2one('pos.session', string='Session', related='pos_order_id.session_id')
company_id = fields.Many2one('res.company', string='Company', related='pos_order_id.company_id')
@@ -0,0 +1,58 @@
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class PosPaymentMethod(models.Model):
""" Used to classify pos.payment.
Generic characteristics of a pos.payment is described in this model.
E.g. A cash payment can be described by a pos.payment.method with
fields: is_cash_count = True and a cash_journal_id set to an
`account.journal` (type='cash') record.
When a pos.payment.method is cash, cash_journal_id is required as
it will be the journal where the account.bank.statement.line records
will be created.
"""
_name = "pos.payment.method"
_description = "Point of Sale Payment Methods"
_order = "id asc"
name = fields.Char(string="Payment Method", required=True)
receivable_account_id = fields.Many2one('account.account',
string='Intermediary Account',
required=True,
domain=[('reconcile', '=', True), ('user_type_id.type', '=', 'receivable')],
default=lambda self: self.env.company.account_default_pos_receivable_account_id,
ondelete='restrict',
help='Account used as counterpart of the income account in the accounting entry representing the pos sales.')
is_cash_count = fields.Boolean(string='Cash')
cash_journal_id = fields.Many2one('account.journal',
string='Cash Journal',
domain=[('type', '=', 'cash')],
ondelete='restrict',
help='The payment method is of type cash. A cash statement will be automatically generated.')
split_transactions = fields.Boolean(
string='Split Transactions',
default=False,
help='If ticked, each payment will generate a separated journal item. Ticking that option will slow the closing of the PoS.')
open_session_ids = fields.Many2many('pos.session', string='Pos Sessions', compute='_compute_open_session_ids', help='Open PoS sessions that are using this payment method.')
config_ids = fields.Many2many('pos.config', string='Point of Sale Configurations')
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
@api.depends('config_ids')
def _compute_open_session_ids(self):
for payment_method in self:
self.open_session_ids = self.env['pos.session'].search([('config_id', 'in', payment_method.config_ids.ids), ('state', '!=', 'closed')])
@api.onchange('is_cash_count')
def _onchange_is_cash_count(self):
if not self.is_cash_count:
self.cash_journal_id = False
def write(self, vals):
if self.open_session_ids:
raise UserError('Kindly close and validate the following open PoS Sessions before modifying this payment method.\n'
'Open sessions: %s' % (' '.join(self.open_session_ids.mapped('name')),))
return super(PosPaymentMethod, self).write(vals)
+544 -70
View File
@@ -1,9 +1,12 @@
# -*- 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, SUPERUSER_ID, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_is_zero
class PosSession(models.Model):
@@ -20,27 +23,6 @@ class PosSession(models.Model):
('closed', 'Closed & Posted'),
]
def _confirm_orders(self):
for session in self:
journal = session.config_id.journal_id
if not journal:
raise UserError(_("You have to set a Sale Journal for the POS:%s") % (session.config_id.name,))
orders = session.order_ids.filtered(lambda order: order.state == 'paid')
orders.with_context(force_company=journal.company_id.id)._create_account_move_line(session)
for order in session.order_ids.filtered(lambda o: o.state not in ['done', 'invoiced']):
if order.state not in ('paid'):
raise UserError(
_("You cannot confirm all orders of this session, because they have not the 'paid' status.\n"
"{reference} is in state {state}, total amount: {total}, paid: {paid}").format(
reference=order.pos_reference or order.name,
state=order.state,
total=order.amount_total,
paid=order.amount_paid,
))
order.action_pos_order_done()
orders_to_reconcile = session.order_ids._filtered_for_reconciliation()
orders_to_reconcile.sudo()._reconcile_payments()
company_id = fields.Many2one('res.company', related='config_id.company_id', string="Company", readonly=True)
config_id = fields.Many2one(
@@ -83,37 +65,59 @@ class PosSession(models.Model):
help="Total of opening cash control lines.",
readonly=True)
cash_register_total_entry_encoding = fields.Monetary(
related='cash_register_id.total_entry_encoding',
compute='_compute_cash_balance',
string='Total Cash Transaction',
readonly=True,
help="Total of all paid sales orders")
cash_register_balance_end = fields.Monetary(
related='cash_register_id.balance_end',
compute='_compute_cash_balance',
digits=0,
string="Theoretical Closing Balance",
help="Sum of opening balance and transactions.",
readonly=True)
cash_register_difference = fields.Monetary(
related='cash_register_id.difference',
compute='_compute_cash_balance',
string='Difference',
help="Difference between the theoretical closing balance and the real closing balance.",
readonly=True)
journal_ids = fields.Many2many(
'account.journal',
related='config_id.journal_ids',
readonly=True,
string='Available Payment Methods')
order_ids = fields.One2many('pos.order', 'session_id', string='Orders')
order_count = fields.Integer(compute='_compute_order_count')
statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Bank Statement', readonly=True)
statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Cash Statements', readonly=True)
picking_count = fields.Integer(compute='_compute_picking_count')
rescue = fields.Boolean(string='Recovery Session',
help="Auto-generated session for orphan orders, ignored in constraints",
readonly=True,
copy=False)
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')
_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
@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}
@@ -133,18 +137,20 @@ class PosSession(models.Model):
action['domain'] = [('id', 'in', pickings.ids)]
return action
@api.depends('config_id', 'statement_ids')
@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
if session.config_id.cash_control:
for statement in session.statement_ids:
if statement.journal_id.type == 'cash':
session.cash_control = True
session.cash_journal_id = statement.journal_id.id
session.cash_register_id = statement.id
if not session.cash_control and session.state != 'closed':
raise UserError(_("Cash control can only be applied to cash journals."))
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('user_id', 'state')
def _check_unicity(self):
@@ -185,31 +191,29 @@ class PosSession(models.Model):
# 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']
statements = []
ABS = self.env['account.bank.statement']
uid = SUPERUSER_ID if self.env.user.has_group('point_of_sale.group_pos_user') else self.env.user.id
for journal in pos_config.journal_ids:
# set the journal_id which should be used by
# account.bank.statement to set the opening balance of the
# newly created bank statement
ctx['journal_id'] = journal.id if pos_config.cash_control and journal.type == 'cash' else False
cash_payment_methods = pos_config.payment_method_ids.filtered(lambda pm: pm.is_cash_count)
statement_ids = self.env['account.bank.statement']
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': journal.id,
'journal_id': cash_journal.id,
'user_id': self.env.user.id,
'name': pos_name,
'balance_start': self.env["account.bank.statement"]._get_opening_balance(journal.id) if journal.type == 'cash' else 0
'balance_start': self.env["account.bank.statement"]._get_opening_balance(cash_journal.id) if cash_journal.type == 'cash' else 0
}
statements.append(ABS.with_context(ctx).with_user(uid).create(st_values).id)
statement_ids |= statement_ids.with_context(ctx).with_user(uid).create(st_values)
values.update({
'name': pos_name,
'statement_ids': [(6, 0, statements)],
'config_id': config_id
'statement_ids': [(6, 0, statement_ids.ids)],
'config_id': config_id,
})
res = super(PosSession, self.with_context(ctx).with_user(uid)).create(values)
@@ -258,17 +262,37 @@ class PosSession(models.Model):
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()
company_id = self.config_id.company_id.id
ctx = dict(self.env.context, force_company=company_id, company_id=company_id, default_partner_type='customer')
ctx_notrack = dict(ctx, mail_notrack=True)
for st in self.statement_ids:
if (st.journal_id.type not in ['bank', 'cash']):
raise UserError(_("The journal type for your payment method should be bank or cash."))
st.with_context(ctx_notrack).sudo().button_confirm_bank()
self.activity_unlink(['point_of_sale.mail_activity_old_session'])
self.with_context(ctx)._confirm_orders()
self._check_if_no_draft_orders()
self._create_account_move()
if self.move_id.line_ids:
self.move_id.post()
else:
# The cash register needs to be confirmed for cash diffs
# made thru cash in/out when sesion is in cash_control.
if self.config_id.cash_control:
self.cash_register_id.button_confirm_bank()
self.move_id.unlink()
self.write({'state': 'closed'})
return {
'type': 'ir.actions.client',
@@ -277,16 +301,456 @@ class PosSession(models.Model):
'params': {'menu_id': self.env.ref('point_of_sale.menu_point_root').id},
}
def action_pos_session_close(self):
# Close CashBox
for st in self.statement_ids:
if any(abs(st.difference) > self.config_id.amount_authorized_diff for st in self.statement_ids):
if not self.user_has_groups("point_of_sale.group_pos_manager"):
raise UserError(_("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, self.config_id.amount_authorized_diff))
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,
'name': journal.sequence_id.next_by_id()
})
self.write({'move_id': account_move.id})
## SECTION: 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}
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_output = defaultdict(amounts)
# 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:
return self._warning_balance_closing()
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
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'):
order_account_move_receivable_lines[move_line.account_id.id] |= move_line
else:
self._validate_session()
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']),
)
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:
# Combine stock lines
stock_moves = self.env['stock.move'].search([
('picking_id', '=', order.picking_id.id),
('company_id.anglo_saxon_accounting', '=', True),
('product_id.categ_id.property_valuation', '=', 'real_time')
])
for move in stock_moves:
exp_key = move.product_id.property_account_expense_id or move.product_id.categ_id.property_account_expense_categ_id
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)
stock_output[out_key] = self._update_amounts(stock_output[out_key], {'amount': amount}, move.picking_id.date)
## SECTION: Create non-reconcilable move lines
# 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)
MoveLine = self.env['account.move.line'].with_context(check_move_validity=False)
tax_vals = [self._get_tax_vals(key, amounts['amount'], amounts['amount_converted'], amounts['base_amount']) for key, amounts in taxes.items()]
# 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)
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()]
)
## SECTION: Create cash statement lines and cash move lines
# 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
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_statement_line_vals(statement, payment.payment_method_id.receivable_account_id, 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])
## SECTION: 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
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():
invoice_receivable_lines[receivable_account_id] = MoveLine.create(vals)
## SECTION: Create stock output lines
# Keep reference to the stock output lines because
# they are reconciled with output lines in the stock.move's account.move.line
stock_output_vals = defaultdict(list)
stock_output_lines = {}
for output_account, amounts in stock_output.items():
stock_output_vals[output_account].append(self._get_stock_output_vals(output_account, amounts['amount'], amounts['amount_converted']))
for output_account, vals in stock_output_vals.items():
stock_output_lines[output_account] = MoveLine.create(vals)
## SECTION: Reconcile account move lines
# reconcile cash receivable lines
for statement in self.statement_ids:
if not self.config_id.cash_control:
statement.write({'balance_end_real': statement.balance_end})
statement.button_confirm_bank()
all_lines = (
split_cash_statement_lines[statement].mapped('journal_entry_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
| combine_cash_statement_lines[statement].mapped('journal_entry_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()
# reconcile invoice receivable lines
for account_id in order_account_move_receivable_lines:
( order_account_move_receivable_lines[account_id]
| invoice_receivable_lines[account_id]
).reconcile()
# reconcile stock output lines
stock_moves = self.env['stock.move'].search([('picking_id', 'in', self.order_ids.filtered(lambda order: not order.is_invoiced).mapped('picking_id').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)
).reconcile()
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.property_account_income_id or product.categ_id.property_account_income_categ_id
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)
price = order_line.price_unit * (1 - (order_line.discount or 0.0) / 100.0)
taxes = tax_ids.compute_all(price_unit=price, quantity=order_line.qty, currency=self.currency_id, is_refund=order_line.qty<0).get('taxes', [])
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,
}
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 = 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]))
base_tags = applied_taxes\
.mapped('invoice_repartition_line_ids' if sign == 1 else 'refund_repartition_line_ids')\
.filtered(lambda line: line.repartition_type == 'base')\
.tag_ids
partial_vals = {
'name': name,
'account_id': account_id,
'move_id': self.move_id.id,
'tax_ids': [(6, 0, tax_ids)],
'tag_ids': [(6, 0, base_tags.ids)],
}
return self._credit_amounts(partial_vals, amount, amount_converted)
def _get_tax_vals(self, key, amount, amount_converted, base_amount):
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': base_amount,
'tax_repartition_line_id': repartition_line_id,
'tag_ids': [(6, 0, tag_ids)],
}
return self._credit_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)
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)
def _get_statement_line_vals(self, statement, receivable_account, amount):
return {
'date': fields.Date.context_today(self),
'amount': amount,
'name': self.name,
'statement_id': statement.id,
'account_id': receivable_account.id,
}
def _update_amounts(self, old_amounts, amounts_to_add, date, round=True):
new_amounts = {}
for k, amount in old_amounts.items():
if k == 'amount_converted':
amount_converted = old_amounts['amount_converted']
amount_to_convert = amounts_to_add['amount']
new_amounts['amount_converted'] = amount_converted if self.is_in_company_currency else (amount_converted + self._amount_converter(amount_to_convert, date, round))
else:
new_amounts[k] = old_amounts[k] + amounts_to_add[k]
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):
""" `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:
return {
'debit': -amount if amount < 0.0 else 0.0,
'credit': amount if amount > 0.0 else 0.0,
**partial_move_line_vals
}
else:
return {
'debit': -amount_converted if amount_converted < 0.0 else 0.0,
'credit': amount_converted if amount_converted > 0.0 else 0.0,
'amount_currency': -amount if amount_converted > 0 else amount,
'currency_id': self.currency_id.id,
**partial_move_line_vals
}
def _debit_amounts(self, partial_move_line_vals, amount, amount_converted):
""" `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:
return {
'debit': amount if amount > 0.0 else 0.0,
'credit': -amount if amount < 0.0 else 0.0,
**partial_move_line_vals
}
else:
return {
'debit': amount_converted if amount_converted > 0.0 else 0.0,
'credit': -amount_converted if amount_converted < 0.0 else 0.0,
'amount_currency': amount if amount_converted > 0 else -amount,
'currency_id': self.currency_id.id,
**partial_move_line_vals
}
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_id': self.env.ref('point_of_sale.view_pos_payment_tree').id,
'view_mode': 'tree',
'domain': [('session_id', '=', self.id)],
'context': {'search_default_group_by_payment_method': 1}
}
def open_frontend_cb(self):
if not self.ids:
@@ -349,6 +813,16 @@ class PosSession(models.Model):
'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'
@@ -365,4 +839,4 @@ class ClosingBalanceConfirm(models.TransientModel):
def confirm_closing_balance(self):
current_session = self.env['pos.session'].browse(self._context['session_id'])
current_session._validate_session()
return current_session._validate_session()
+1 -1
View File
@@ -18,4 +18,4 @@ class ResCompany(models.Model):
sessions_in_period = pos_session_model.search([('state', '!=', 'closed'), '|', ('start_at', '<=', record.period_lock_date), ('start_at', '<=', record.fiscalyear_lock_date)])
if sessions_in_period:
sessions_str = ', '.join(sessions_in_period.mapped('name'))
raise ValidationError(_("Please close all the point of sale sessions in this period before closing it. Open sessions are: %s ") % (sessions_str))
raise ValidationError(_("Please close all the point of sale sessions in this period before closing it. Open sessions are: %s ") % (sessions_str))
@@ -37,3 +37,6 @@ access_barcode_rule_pos_manager,barcode.rule.pos.manager,barcodes.model_barcode_
access_bank_statement_cashbox_user,account.bank.statement.cashbox user,account.model_account_bank_statement_cashbox,group_pos_user,1,1,1,0
access_account_cashbox_line_user,account.cashbox.line user,account.model_account_cashbox_line,group_pos_user,1,1,1,1
access_decimal_precision_user,decimal.precision,base.model_decimal_precision,group_pos_user,1,0,0,0
access_pos_payment_user,pos.payment user,model_pos_payment,group_pos_user,1,1,1,1
access_pos_payment_method_user,pos.payment.method user,model_pos_payment_method,group_pos_user,1,0,0,0
access_pos_payment_method_manager,pos.payment.method manager,model_pos_payment_method,group_pos_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
37 access_bank_statement_cashbox_user account.bank.statement.cashbox user account.model_account_bank_statement_cashbox group_pos_user 1 1 1 0
38 access_account_cashbox_line_user account.cashbox.line user account.model_account_cashbox_line group_pos_user 1 1 1 1
39 access_decimal_precision_user decimal.precision base.model_decimal_precision group_pos_user 1 0 0 0
40 access_pos_payment_user pos.payment user model_pos_payment group_pos_user 1 1 1 1
41 access_pos_payment_method_user pos.payment.method user model_pos_payment_method group_pos_user 1 0 0 0
42 access_pos_payment_method_manager pos.payment.method manager model_pos_payment_method group_pos_manager 1 1 1 1
@@ -71,6 +71,18 @@
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="rule_pos_payment_method_multi_company" model="ir.rule">
<field name="name">PoS Payment Method</field>
<field name="model_id" ref="model_pos_payment_method" />
<field name="global" eval="True" />
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record id="rule_pos_payment_multi_company" model="ir.rule">
<field name="name">PoS Payment</field>
<field name="model_id" ref="model_pos_payment" />
<field name="global" eval="True" />
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<data noupdate="1">
<record id="base.default_user" model="res.users">
+30 -63
View File
@@ -57,7 +57,6 @@ exports.PosModel = Backbone.Model.extend({
this.employee = {name: null, id: null, barcode: null, user_id:null, pin:null};
this.employees = [];
this.partners = [];
this.cashregisters = [];
this.taxes = [];
this.pos_session = null;
this.config = null;
@@ -250,10 +249,11 @@ exports.PosModel = Backbone.Model.extend({
},
},{
model: 'pos.session',
fields: ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number','login_number'],
fields: ['id', 'name', 'user_id', 'config_id', 'start_at', 'stop_at', 'sequence_number', 'login_number', 'payment_method_ids'],
domain: function(self){ return [['state','=','opened'],['user_id','=',session.uid]]; },
loaded: function(self,pos_sessions){
loaded: function(self,pos_sessions,tmp){
self.pos_session = pos_sessions[0];
tmp.payment_method_ids = pos_sessions[0].payment_method_ids;
},
},{
model: 'pos.config',
@@ -365,54 +365,28 @@ exports.PosModel = Backbone.Model.extend({
}));
},
},{
model: 'account.bank.statement',
fields: ['account_id','currency_id','journal_id','state','name','user_id','pos_session_id'],
domain: function(self){ return [['state', '=', 'open'],['pos_session_id', '=', self.pos_session.id]]; },
loaded: function(self, cashregisters, tmp){
self.cashregisters = cashregisters;
tmp.journals = [];
_.each(cashregisters,function(statement){
tmp.journals.push(statement.journal_id[0]);
});
model: 'pos.payment.method',
fields: ['name', 'is_cash_count'],
domain: function(self, tmp) {
return [['id', 'in', tmp.payment_method_ids]];
},
},{
model: 'account.journal',
fields: ['type', 'sequence'],
domain: function(self,tmp){ return [['id','in',tmp.journals]]; },
loaded: function(self, journals){
var i;
self.journals = journals;
// associate the bank statements with their journals.
var cashregisters = self.cashregisters;
var ilen = cashregisters.length;
for(i = 0; i < ilen; i++){
for(var j = 0, jlen = journals.length; j < jlen; j++){
if(cashregisters[i].journal_id[0] === journals[j].id){
cashregisters[i].journal = journals[j];
}
}
}
self.cashregisters_by_id = {};
for (i = 0; i < self.cashregisters.length; i++) {
self.cashregisters_by_id[self.cashregisters[i].id] = self.cashregisters[i];
}
self.cashregisters = self.cashregisters.sort(function(a,b){
// prefer cashregisters to be first in the list
if (a.journal.type == "cash" && b.journal.type != "cash") {
loaded: function(self, payment_methods) {
self.payment_methods = payment_methods.sort(function(a,b){
// prefer cash payment_method to be first in the list
if (a.is_cash_count && !b.is_cash_count) {
return -1;
} else if (a.journal.type != "cash" && b.journal.type == "cash") {
} else if (!a.is_cash_count && b.is_cash_count) {
return 1;
} else {
return a.journal.sequence - b.journal.sequence;
return a.id - b.id;
}
});
},
}, {
self.payment_methods_by_id = {}
_.each(self.payment_methods, function(payment_method) {
self.payment_methods_by_id[payment_method.id] = payment_method
})
}
},{
model: 'account.fiscal.position',
fields: [],
domain: function(self){ return [['id','in',self.config.fiscal_position_ids]]; },
@@ -2039,16 +2013,15 @@ exports.Paymentline = Backbone.Model.extend({
this.init_from_JSON(options.json);
return;
}
this.cashregister = options.cashregister;
if (this.cashregister === undefined) {
this.payment_method = options.payment_method;
if (this.payment_method === undefined) {
throw new Error(_t('Please configure a payment method in your POS.'));
}
this.name = this.cashregister.journal_id[1];
this.name = this.payment_method.name;
},
init_from_JSON: function(json){
this.amount = json.amount;
this.cashregister = this.pos.cashregisters_by_id[json.statement_id];
this.name = this.cashregister.journal_id[1];
this.payment_method = this.pos.payment_methods_by_id[json.payment_method_id];
},
//sets the amount of money on this payment line
set_amount: function(value){
@@ -2069,18 +2042,12 @@ exports.Paymentline = Backbone.Model.extend({
this.trigger('change',this);
}
},
// returns the payment type: 'cash' | 'bank'
get_type: function(){
return this.cashregister.journal.type;
},
// returns the associated cashregister
//exports as JSON for server communication
export_as_JSON: function(){
return {
name: time.datetime_to_str(new Date()),
statement_id: this.cashregister.id,
account_id: this.cashregister.account_id[0],
journal_id: this.cashregister.journal_id[0],
payment_method_id: this.payment_method.id,
amount: this.get_amount()
};
},
@@ -2088,7 +2055,7 @@ exports.Paymentline = Backbone.Model.extend({
export_for_printing: function(){
return {
amount: this.get_amount(),
journal: this.cashregister.journal_id[1],
payment_method: this.payment_method.name,
};
},
});
@@ -2574,12 +2541,12 @@ exports.Order = Backbone.Model.extend({
},
/* ---- Payment Lines --- */
add_paymentline: function(cashregister) {
add_paymentline: function(payment_method) {
this.assert_editable();
var newPaymentline = new exports.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos});
if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){
var newPaymentline = new exports.Paymentline({},{order: this, payment_method:payment_method, pos: this.pos});
if(!payment_method.is_cash_count || this.pos.config.iface_precompute_cash){
newPaymentline.set_amount( this.get_due() );
}
};
this.paymentlines.add(newPaymentline);
this.select_paymentline(newPaymentline);
@@ -2751,7 +2718,7 @@ exports.Order = Backbone.Model.extend({
},
is_paid_with_cash: function(){
return !!this.paymentlines.find( function(pl){
return pl.cashregister.journal.type === 'cash';
return pl.payment_method.is_cash_count;
});
},
finalize: function(){
+5 -11
View File
@@ -1901,7 +1901,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
}
if (! open_paymentline) {
this.pos.get_order().add_paymentline( this.pos.cashregisters[0]);
this.pos.get_order().add_paymentline( this.pos.payment_methods[0]);
this.render_paymentlines();
}
@@ -1971,14 +1971,8 @@ var PaymentScreenWidget = ScreenWidget.extend({
lines.appendTo(this.$('.paymentlines-container'));
},
click_paymentmethods: function(id) {
var cashregister = null;
for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
if ( this.pos.cashregisters[i].journal_id[0] === id ){
cashregister = this.pos.cashregisters[i];
break;
}
}
this.pos.get_order().add_paymentline( cashregister );
var payment_method = this.pos.payment_methods_by_id[id]
this.pos.get_order().add_paymentline(payment_method);
this.reset_input();
this.render_paymentlines();
},
@@ -2132,8 +2126,8 @@ var PaymentScreenWidget = ScreenWidget.extend({
// The exact amount must be paid if there is no cash payment method defined.
if (Math.abs(order.get_total_with_tax() - order.get_total_paid()) > 0.00001) {
var cash = false;
for (var i = 0; i < this.pos.cashregisters.length; i++) {
cash = cash || (this.pos.cashregisters[i].journal.type === 'cash');
for (var i = 0; i < this.pos.payment_methods.length; i++) {
cash = cash || (this.pos.payment_methods[i].is_cash_count);
}
if (!cash) {
this.gui.show_popup('error',{
+3 -4
View File
@@ -652,13 +652,12 @@
<t t-name="PaymentScreen-Paymentmethods">
<div class='paymentmethods'>
<t t-foreach="widget.pos.cashregisters" t-as="cashregister">
<div class="button paymentmethod" t-att-data-id="cashregister.journal_id[0]">
<t t-esc="cashregister.journal_id[1]" />
<t t-foreach="widget.pos.payment_methods" t-as="payment_method">
<div class="button paymentmethod" t-att-data-id="payment_method.id">
<t t-esc="payment_method.name" />
</div>
</t>
</div>
</t>
<t t-name="PaymentScreenWidget">
@@ -1,47 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_account_journal_form" model="ir.actions.act_window">
<field name="name">Payment Methods</field>
<field name="res_model">account.journal</field>
<field name="view_mode">tree,kanban,form</field>
<field name="context" eval="{'search_default_available_in_pos': True, 'default_journal_user': True, 'default_type': 'cash'}"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new payment method
</p><p>
Payment methods are defined by accounting journals having the
field <i>PoS Payment Method</i> checked. In order to be useable
from the touchscreen interface, you must set the payment method
on the <i>Point of Sale</i> configuration.
</p>
</field>
</record>
<menuitem id="menu_action_account_journal_form_open"
action="action_account_journal_form"
parent="point_of_sale.menu_point_config_product"
groups="base.group_no_one"
sequence="30"/>
<record id="view_account_journal_search_inherit_point_of_sale" model="ir.ui.view">
<field name="name">account.journal.filter</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.view_account_journal_search" />
<field name="arch" type="xml">
<xpath expr="//filter[@name='inactive']" position="before">
<filter name="available_in_pos" string="Available for Point of Sale" domain="[('journal_user', '=', True)]"/>
</xpath>
</field>
</record>
<record id="account_journal_action_point_of_sale" model="ir.actions.act_window">
<field name="name">POS Journals</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.journal</field>
<field name="view_mode">tree,form,kanban</field>
<field name="search_view_id" ref="view_account_journal_search_inherit_point_of_sale"/>
<field name="context">{'search_default_available_in_pos': True, 'default_journal_user': True, 'default_type': 'cash'}</field>
</record>
<!-- View Taxes -->
@@ -40,9 +40,6 @@
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.view_account_journal_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='type']" position="after">
<field name="journal_user" attrs="{'invisible': [('type', 'not in', ['bank', 'cash'])]}"/>
</xpath>
<xpath expr="//notebook[last()]" position="inside">
<page string="Point of Sale" name="point_of_sale" invisible="1"/>
</xpath>
@@ -14,7 +14,7 @@
parent="menu_point_root"
sequence="10"/>
<menuitem id="menu_point_of_sale_customer"
<menuitem id="menu_point_of_sale_customer"
name="Customers"
parent="menu_point_of_sale"
action="base.action_partner_customer_form"
@@ -311,17 +311,17 @@
</div>
<h2>Payments</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box" id="payment_methods">
<div class="col-12 col-lg-6 o_setting_box" id="payment_methods_new">
<div class="o_setting_right_pane">
<span class="o_form_label">Payment Methods</span>
<div class="text-muted">
Payment methods available
</div>
<div class="content-group mt16">
<field name="journal_ids" required="1" colspan="4" nolabel="1" context="{'default_journal_user': 1, 'default_type': 'cash'}" widget="many2many_tags"/>
<field name="payment_method_ids" colspan="4" nolabel="1" context="{}" widget="many2many_tags"/>
</div>
<div>
<button name="%(account_journal_action_point_of_sale)d" icon="fa-arrow-right" type="action" string="Payment Methods" class="btn-link"/>
<button name="%(action_payment_methods_tree)d" icon="fa-arrow-right" type="action" string="Payment Methods" class="btn-link"/>
</div>
</div>
</div>
@@ -473,10 +473,6 @@
<label string="Sales Journal" for="journal_id" class="col-lg-3 o_light_label" widget="selection"/>
<field name="journal_id" required="1" domain="[('company_id', '=', company_id), ('type', '=', 'sale')]" context="{'default_company_id': company_id, 'default_type': 'sale'}"/>
</div>
<div class="row" groups="account.group_account_user" title="Get one journal item per product rather than one journal item per receipt line. This works for any anonymous order. If the customer is set on the order, one journal item is created for each receipt line. This option is recommended for an easy review of your journal entries when managing lots of orders.">
<label string="Group Journal Items" for="group_by" class="col-lg-3 o_light_label"/>
<field name="group_by"/>
</div>
</div>
</div>
</div>
+22 -17
View File
@@ -6,7 +6,7 @@
<field name="arch" type="xml">
<form string="Point of Sale Orders" create="0">
<header>
<button name="%(action_pos_payment)d" string="Payment" class="oe_highlight" type="action" states="draft" context="{'pos_session_id' : session_id}"/>
<button name="%(action_pos_payment)d" string="Payment" class="oe_highlight" type="action" states="draft" />
<button name="action_pos_order_invoice" string="Invoice" type="object"
attrs="{'invisible': ['|', ('invoice_group', '=', False), ('state','!=','paid')]}"/>
<button name="refund" string="Return Products" type="object"
@@ -78,27 +78,32 @@
<div class="oe_clear"/>
</page>
<page string="Payments">
<field name="statement_ids" colspan="4" nolabel="1">
<tree editable="bottom" string="Statement lines">
<field name="journal_id"/>
<field name="statement_id"/>
<field name="payment_ids" colspan="4" nolabel="1">
<tree string="Payments">
<field name="currency_id" invisible="1" />
<field name="payment_date"/>
<field name="payment_method_id"/>
<field name="amount"/>
</tree>
</field>
</page>
<page name="extra" string="Extra Info">
<group string="General Information">
<field name="company_id" groups="base.group_multi_company"/>
<field name="location_id" widget="selection" groups="stock.group_stock_multi_locations"/>
<field name="pricelist_id" groups="product.group_sale_pricelist"/>
<field name="picking_id" readonly="1"/>
<field name="pos_reference"/>
</group>
<group string="Accounting Information" groups="account.group_account_manager">
<group>
<field name="sale_journal" domain="[('type','=','sale')]"/>
<field name="account_move" readonly="1"
attrs="{'invisible':[('state','!=','done')]}"/>
<group >
<group string="Inventory">
<field name="location_id" widget="selection" groups="stock.group_stock_multi_locations"/>
<field name="picking_id" readonly="1"/>
</group>
<group
string="Accounting"
groups="account.group_account_manager"
attrs="{'invisible':['|', ('session_move_id','=', False), ('state', '=', 'invoiced')]}"
>
<field name="session_move_id" readonly="1" />
</group>
<group string="Other Information">
<field name="pos_reference"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="pricelist_id" groups="product.group_sale_pricelist"/>
</group>
</group>
</page>
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_payment_method_view_form" model="ir.ui.view">
<field name="name">pos.payment.method.form</field>
<field name="model">pos.payment.method</field>
<field name="arch" type="xml">
<form string="Payment Methods">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name" class="oe_inline"/></h1>
</div>
<group name="Payment methods">
<group>
<field name="receivable_account_id" groups="account.group_account_user" />
<field name="is_cash_count" />
<field name="cash_journal_id" attrs="{'invisible': [('is_cash_count', '=', False)], 'required': [('is_cash_count', '=', True)]}" />
<field name="split_transactions" groups="base.group_no_one" />
<field name="company_id" readonly="1" groups="base.group_multi_company" />
</group>
<group>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="pos_payment_method_view_tree" model="ir.ui.view">
<field name="name">pos.payment.method.tree</field>
<field name="model">pos.payment.method</field>
<field name="arch" type="xml">
<tree string="Payment Methods">
<field name="name" />
<field name="receivable_account_id" groups="account.group_account_user"/>
<field name="is_cash_count" />
<field name="split_transactions" groups="base.group_no_one" />
<field name="company_id" groups="base.group_multi_company" />
</tree>
</field>
</record>
<record id="pos_payment_method_view_search" model="ir.ui.view">
<field name="name">pos.payment.search.view</field>
<field name="model">pos.payment.method</field>
<field name="arch" type="xml">
<search string="Payment Methods">
<field name="name"/>
<field name="receivable_account_id" groups="account.group_account_user" />
<group expand="1" string="Group By">
<filter name="group_by_receivable_account" string="Account" domain="[]" context="{'group_by':'receivable_account_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_pos_payment_method_form" model="ir.actions.act_window">
<field name="name">Payment Methods</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos.payment.method</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" eval="False"/>
<field name="domain">[]</field>
<field name="context">{'search_default_group_by_account': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new payment method
</p>
<p>
Installing chart of accounts from the General Settings of
Invocing/Accounting app will create Bank and Cash payment
methods automatically.
</p>
</field>
</record>
<menuitem id="menu_pos_payment_method" parent="menu_point_config_product" action="action_pos_payment_method_form" sequence="3" groups="group_pos_manager,group_pos_user"/>
<record id="action_payment_methods_tree" model="ir.actions.act_window">
<field name="context">{}</field>
<field name="name">Payments Methods</field>
<field name="res_model">pos.payment.method</field>
<field name="view_id" ref="pos_payment_method_view_tree"/>
<field name="view_mode">tree,form,kanban</field>
</record>
</odoo>
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pos_payment_form" model="ir.ui.view">
<field name="name">pos.payment.form</field>
<field name="model">pos.payment</field>
<field name="arch" type="xml">
<form string="Payments" create="0">
<sheet>
<group>
<field name="currency_id" invisible="1" />
<field name="name" />
<field name="amount" />
<field name="pos_order_id" />
<field name="payment_method_id" />
<field name="session_id" />
</group>
</sheet>
</form>
</field>
</record>
<record id="view_pos_payment_tree" model="ir.ui.view">
<field name="name">pos.payment.tree</field>
<field name="model">pos.payment</field>
<field name="arch" type="xml">
<tree string="Payments" create="0">
<field name="currency_id" invisible="1" />
<field name="payment_date" widget="date" />
<field name="payment_method_id" />
<field name="pos_order_id" />
<field name="amount" />
</tree>
</field>
</record>
<record id="view_pos_payment_search" model="ir.ui.view">
<field name="name">pos.payment.search.view</field>
<field name="model">pos.payment</field>
<field name="arch" type="xml">
<search string="Payments">
<field name="name"/>
<field name="amount"/>
<field name="pos_order_id" />
<group expand="0" string="Group By">
<filter name="group_by_payment_method" string="Payment Method" domain="[]" context="{'group_by':'payment_method_id'}"/>
<filter name="group_by_session" string="Session" domain="[]" context="{'group_by':'session_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_pos_payment_form" model="ir.actions.act_window">
<field name="name">Payments</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos.payment</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" eval="False"/>
<field name="domain">[]</field>
<field name="context">{'search_default_group_by_payment_method': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No orders found
</p><p>
To record new orders, start a new session.
</p>
</field>
</record>
<menuitem id="menu_pos_payment" parent="menu_point_of_sale" action="action_pos_payment_form" sequence="3" groups="group_pos_manager,group_pos_user"/>
</odoo>
+19 -14
View File
@@ -36,6 +36,24 @@
<button class="oe_stat_button" name="action_stock_picking" type="object" icon="fa-arrows-v" attrs="{'invisible':[('picking_count', '=', 0)]}">
<field name="picking_count" widget="statinfo" string="Picking Errors"/>
</button>
<button
name="action_show_payments_list"
type="object"
class="oe_stat_button"
icon="fa-dollar"
>
<field name="total_payments_amount" widget="statinfo" string="Payments"/>
</button>
<button
name="show_journal_items"
type="object"
class="oe_stat_button"
icon="fa-bars"
string="Journal Items"
attrs="{'invisible':[('move_id', '=', False)]}"
groups="account.group_account_user"
>
</button>
</div>
<h1 class="oe_title">
Session:
@@ -46,6 +64,7 @@
<field name="user_id"/>
<field name="currency_id" invisible="1"/>
<field name="config_id"/>
<field name="move_id" readonly="1" groups="account.group_account_user" />
<field name="start_at" attrs="{'invisible' : [('state', '=', 'opening_control')]}"/>
<field name="stop_at" attrs="{'invisible' : [('state', '!=', 'closed')]}"/>
</group>
@@ -89,20 +108,6 @@
</group>
</div>
</page>
<page string="Payments" attrs="{'invisible' : [('state', 'in', ('new_session', 'opening_control'))]}">
<field name="statement_ids">
<tree string="Statements">
<field name="name" />
<field name="journal_id" />
<field name="balance_start" />
<field name="total_entry_encoding" />
<field name="balance_end_real" />
<field name="difference" />
<field name="currency_id" groups="base.group_multi_currency" />
<field name="state" />
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
-1
View File
@@ -3,5 +3,4 @@
from . import pos_box
from . import pos_details
from . import pos_open_statement
from . import pos_payment
@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pos_open_statement" model="ir.ui.view">
<field name="name">pos.open.statement.form</field>
<field name="model">pos.open.statement</field>
<field name="arch" type="xml">
<form string="Open Statements">
<separator string="Do you want to open cash registers?"/>
<span class="o_form_label">The system will open all cash registers, so that you can start recording payments. We suggest you to control the opening balance of each register, using their CashBox tab.</span>
<footer>
<button name="open_statement" type="object" string="Open Registers" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<act_window
id="act_pos_open_statement"
name="Open Statements"
res_model="pos.open.statement"
binding_model="account.bank.statement"
view_mode="form"
target="new"/>
<record id="action_pos_open_statement" model="ir.actions.act_window">
<field name="name">Open Cash Register</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos.open.statement</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>
+29 -32
View File
@@ -7,40 +7,33 @@ from odoo.tools import float_is_zero
class PosMakePayment(models.TransientModel):
_name = 'pos.make.payment'
_description = 'Point of Sale Payment'
_description = 'Point of Sale Make Payment Wizard'
def _default_session(self):
def _default_config(self):
active_id = self.env.context.get('active_id')
if active_id:
return self.env['pos.order'].browse(active_id).session_id
return False
def _default_journal(self):
active_id = self.env.context.get('active_id')
if active_id:
session = self.env['pos.order'].browse(active_id).session_id
return session.config_id.journal_ids and session.config_id.journal_ids.ids[0] or False
return self.env['pos.order'].browse(active_id).session_id.config_id
return False
def _default_amount(self):
active_id = self.env.context.get('active_id')
if active_id:
order = self.env['pos.order'].browse(active_id)
return (order.amount_total - order.amount_paid)
return order.amount_total - order.amount_paid
return False
session_id = fields.Many2one('pos.session', required=True, default=_default_session)
journal_id = fields.Many2one('account.journal', string='Payment Mode', required=True, default=_default_journal)
amount = fields.Float(digits=0, required=True, default=_default_amount)
payment_name = fields.Char(string='Payment Reference')
payment_date = fields.Date(string='Payment Date', required=True, default=lambda self: fields.Date.context_today(self))
def _default_payment_method(self):
active_id = self.env.context.get('active_id')
if active_id:
order_id = self.env['pos.order'].browse(active_id)
return order_id.session_id.payment_method_ids.sorted(lambda pm: pm.is_cash_count, reverse=True)[:1]
return False
@api.onchange('session_id')
def _on_change_session(self):
if self.session_id:
return {
'domain': {'journal_id': [('id', 'in', self.session_id.config_id.journal_ids.ids)]}
}
config_id = fields.Many2one('pos.config', string='Point of Sale Configuration', required=True, default=_default_config)
amount = fields.Float(digits=0, required=True, default=_default_amount)
payment_method_id = fields.Many2one('pos.payment.method', string='Payment Method', required=True, default=_default_payment_method)
payment_name = fields.Char(string='Payment Reference')
payment_date = fields.Datetime(string='Payment Date', required=True, default=lambda self: fields.Datetime.now())
def check(self):
"""Check the order:
@@ -48,18 +41,23 @@ class PosMakePayment(models.TransientModel):
if the order is paid print ticket.
"""
self.ensure_one()
order = self.env['pos.order'].browse(self.env.context.get('active_id', False))
currency = order.pricelist_id.currency_id
amount = order.amount_total - order.amount_paid
data = self.read()[0]
# add_payment expect a journal key
data['journal'] = data['journal_id'][0]
data['amount'] = currency.round(data['amount']) if currency else data['amount']
if not float_is_zero(amount, precision_rounding=currency.rounding or 0.01):
order.add_payment(data)
if order.test_paid():
currency = order.currency_id
init_data = self.read()[0]
if not float_is_zero(init_data['amount'], precision_rounding=currency.rounding):
order.add_payment({
'pos_order_id': order.id,
'amount': currency.round(init_data['amount']) if currency else init_data['amount'],
'name': init_data['payment_name'],
'payment_method_id': init_data['payment_method_id'][0],
})
if float_is_zero(order.amount_total - order.amount_paid, precision_rounding=currency.rounding):
order.action_pos_order_paid()
return {'type': 'ir.actions.act_window_close'}
return self.launch_payment()
def launch_payment(self):
@@ -73,4 +71,3 @@ class PosMakePayment(models.TransientModel):
'type': 'ir.actions.act_window',
'context': self.env.context,
}
+2 -2
View File
@@ -6,8 +6,8 @@
<field name="arch" type="xml">
<form string="Pay Order">
<group>
<field name="session_id" invisible="True"/>
<field name="journal_id" domain="[('journal_user', '=', True)]"/>
<field name="config_id" invisible="1" />
<field name="payment_method_id" domain="[('config_ids', 'in', config_id)]"/>
<field name="amount" />
<field name="payment_name"/>
</group>
+12 -38
View File
@@ -26,8 +26,8 @@ class PosMercuryConfiguration(models.Model):
merchant_pwd = fields.Char(string='Merchant Password', required=True, help='Password of the merchant to authenticate him on the payment provider server')
class AccountBankStatementLine(models.Model):
_inherit = "account.bank.statement.line"
class PoSPayment(models.Model):
_inherit = "pos.payment"
mercury_card_number = fields.Char(string='Card Number', help='The last 4 numbers of the card used to pay')
mercury_prefixed_card_number = fields.Char(string='Card Number Prefix', compute='_compute_prefixed_card_number', help='The card number used for the payment.')
@@ -45,8 +45,8 @@ class AccountBankStatementLine(models.Model):
line.mercury_prefixed_card_number = ""
class AccountJournal(models.Model):
_inherit = 'account.journal'
class PoSPaymentMethod(models.Model):
_inherit = 'pos.payment.method'
pos_mercury_config_id = fields.Many2one('pos_mercury.configuration', string='Mercury Credentials', help='The configuration of Mercury used for this journal')
@@ -55,46 +55,20 @@ class PosOrder(models.Model):
_inherit = "pos.order"
@api.model
def _payment_fields(self, ui_paymentline):
fields = super(PosOrder, self)._payment_fields(ui_paymentline)
def _payment_fields(self, order, ui_paymentline):
fields = super(PosOrder, self)._payment_fields(order, ui_paymentline)
fields.update({
'card_number': ui_paymentline.get('mercury_card_number'),
'card_brand': ui_paymentline.get('mercury_card_brand'),
'card_owner_name': ui_paymentline.get('mercury_card_owner_name'),
'ref_no': ui_paymentline.get('mercury_ref_no'),
'record_no': ui_paymentline.get('mercury_record_no'),
'invoice_no': ui_paymentline.get('mercury_invoice_no')
'mercury_card_number': ui_paymentline.get('mercury_card_number'),
'mercury_card_brand': ui_paymentline.get('mercury_card_brand'),
'mercury_card_owner_name': ui_paymentline.get('mercury_card_owner_name'),
'mercury_ref_no': ui_paymentline.get('mercury_ref_no'),
'mercury_record_no': ui_paymentline.get('mercury_record_no'),
'mercury_invoice_no': ui_paymentline.get('mercury_invoice_no')
})
return fields
def add_payment(self, data):
statement_id = super(PosOrder, self).add_payment(data)
statement_lines = self.env['account.bank.statement.line'].search([('statement_id', '=', statement_id),
('pos_statement_id', '=', self.id),
('journal_id', '=', data['journal'])])
statement_lines = statement_lines.filtered(lambda line: float_compare(line.amount, data['amount'],
precision_rounding=line.journal_currency_id.rounding) == 0)
# we can get multiple statement_lines when there are >1 credit
# card payments with the same amount. In that case it doesn't
# matter which statement line we pick, just pick one that
# isn't already used.
for line in statement_lines:
if not line.mercury_card_brand:
line.mercury_card_brand = data.get('card_brand')
line.mercury_card_number = data.get('card_number')
line.mercury_card_owner_name = data.get('card_owner_name')
line.mercury_ref_no = data.get('ref_no')
line.mercury_record_no = data.get('record_no')
line.mercury_invoice_no = data.get('invoice_no')
break
return statement_id
class AutoVacuum(models.AbstractModel):
_inherit = 'ir.autovacuum'
@@ -25,20 +25,20 @@ class MercuryTransaction(models.Model):
return pos_session
def _get_pos_mercury_config_id(self, config, journal_id):
journal = config.journal_ids.filtered(lambda r: r.id == journal_id)
def _get_pos_mercury_config_id(self, config, payment_method_id):
payment_method = config.current_session_id.payment_method_ids.filtered(lambda pm: pm.id == payment_method_id)
if journal and journal.pos_mercury_config_id:
return journal.pos_mercury_config_id
if payment_method and payment_method.pos_mercury_config_id:
return payment_method.pos_mercury_config_id
else:
raise UserError(_("No Mercury configuration associated with the journal."))
raise UserError(_("No Mercury configuration associated with the payment method."))
def _setup_request(self, data):
# todo: in master make the client include the pos.session id and use that
pos_session = self._get_pos_session()
config = pos_session.config_id
pos_mercury_config = self._get_pos_mercury_config_id(config, data['journal_id'])
pos_mercury_config = self._get_pos_mercury_config_id(config, data['payment_method_id'])
data['operator_id'] = pos_session.user_id.login
data['merchant_id'] = pos_mercury_config.sudo().merchant_id
+22 -39
View File
@@ -13,31 +13,19 @@ var PopupWidget = require('point_of_sale.popups');
var ScreenWidget = screens.ScreenWidget;
var PaymentScreenWidget = screens.PaymentScreenWidget;
pos_model.load_fields("account.journal", "pos_mercury_config_id");
pos_model.load_fields('pos.payment.method', 'pos_mercury_config_id');
pos_model.PosModel = pos_model.PosModel.extend({
getOnlinePaymentJournals: function () {
var self = this;
var online_payment_journals = [];
getOnlinePaymentMethods: function () {
var online_payment_methods = [];
$.each(this.journals, function (i, val) {
if (val.pos_mercury_config_id) {
online_payment_journals.push({label:self.getCashRegisterByJournalID(val.id).journal_id[1], item:val.id});
$.each(this.payment_methods, function (i, payment_method) {
if (payment_method.pos_mercury_config_id) {
online_payment_methods.push({label: payment_method.name, item: payment_method.id});
}
});
return online_payment_journals;
},
getCashRegisterByJournalID: function (journal_id) {
var cashregister_return;
$.each(this.cashregisters, function (index, cashregister) {
if (cashregister.journal_id[0] === journal_id) {
cashregister_return = cashregister;
}
});
return cashregister_return;
return online_payment_methods;
},
decodeMagtek: function (magtekInput) {
// Regular expression to identify and extract data from the track 1 & 2 of the magnetic code
@@ -258,7 +246,7 @@ ScreenWidget.include({
show: function () {
this._super();
if(this.pos.getOnlinePaymentJournals().length !== 0) {
if(this.pos.getOnlinePaymentMethods().length !== 0) {
this.pos.barcode_reader.set_action_callback('credit', _.bind(this.credit_error_action, this));
}
}
@@ -348,7 +336,7 @@ PaymentScreenWidget.include({
return;
}
if(this.pos.getOnlinePaymentJournals().length === 0) {
if(this.pos.getOnlinePaymentMethods().length === 0) {
return;
}
@@ -379,7 +367,7 @@ PaymentScreenWidget.include({
'transaction_code' : 'Sale',
'invoice_no' : self.pos.get_order().uid.replace(/-/g,''),
'purchase' : purchase_amount,
'journal_id' : parsed_result.journal_id,
'payment_method_id' : parsed_result.payment_method_id,
};
var def = old_deferred || new $.Deferred();
@@ -426,7 +414,7 @@ PaymentScreenWidget.include({
}
var response = self.pos.decodeMercuryResponse(data);
response.journal_id = parsed_result.journal_id;
response.payment_method_id = parsed_result.payment_method_id;
if (response.status === 'Approved') {
// AP* indicates a duplicate request, so don't add anything for those
@@ -443,7 +431,7 @@ PaymentScreenWidget.include({
if (swipe_pending_line) {
order.select_paymentline(swipe_pending_line);
} else {
order.add_paymentline(self.pos.getCashRegisterByJournalID(parsed_result.journal_id));
order.add_paymentline(self.payment_methods_by_id[parsed_result.payment_method_id]);
}
order.selected_paymentline.paid = true;
@@ -502,17 +490,17 @@ PaymentScreenWidget.include({
credit_code_action: function (parsed_result) {
var self = this;
var online_payment_journals = this.pos.getOnlinePaymentJournals();
var online_payment_methods = this.pos.getOnlinePaymentMethods();
if (online_payment_journals.length === 1) {
parsed_result.journal_id = online_payment_journals[0].item;
if (online_payment_methods.length === 1) {
parsed_result.payment_method_id = online_payment_methods[0].item;
self.credit_code_transaction(parsed_result);
} else { // this is for supporting another payment system like mercury
this.gui.show_popup('selection',{
title: _t('Pay with: '),
list: online_payment_journals,
list: online_payment_methods,
confirm: function (item) {
parsed_result.journal_id = item;
parsed_result.payment_method_id = item;
self.credit_code_transaction(parsed_result);
},
cancel: self.credit_code_cancel,
@@ -628,20 +616,15 @@ PaymentScreenWidget.include({
click_paymentmethods: function (id) {
var i;
var order = this.pos.get_order();
var cashregister = null;
for (i = 0; i < this.pos.cashregisters.length; i++) {
if (this.pos.cashregisters[i].journal_id[0] === id){
cashregister = this.pos.cashregisters[i];
break;
}
}
var payment_method = this.pos.payment_methods_by_id[id]
// this.pos.get_order().add_paymentline(payment_method);
if (cashregister.journal.pos_mercury_config_id) {
if (payment_method.pos_mercury_config_id) {
var already_swipe_pending = false;
var lines = order.get_paymentlines();
for (i = 0; i < lines.length; i++) {
if (lines[i].cashregister.journal.pos_mercury_config_id && lines[i].mercury_swipe_pending) {
if (lines[i].payment_method.pos_mercury_config_id && lines[i].mercury_swipe_pending) {
already_swipe_pending = true;
}
}
@@ -666,7 +649,7 @@ PaymentScreenWidget.include({
show: function () {
this._super();
if (this.pos.getOnlinePaymentJournals().length !== 0) {
if (this.pos.getOnlinePaymentMethods().length !== 0) {
this.pos.barcode_reader.set_action_callback('credit', _.bind(this.credit_code_action, this));
}
},
@@ -12,7 +12,7 @@
<t t-extend="PaymentScreen-Paymentlines">
<t t-jquery=".col-name" t-operation="inner">
<t t-if="line.cashregister.journal.type === 'bank'">
<t t-if="! line.payment_method.is_cash_count">
<t t-if="line.mercury_swipe_pending">
<div>WAITING FOR SWIPE</div>
</t>
@@ -20,7 +20,7 @@
<t t-esc='line.name' />
</t>
</t>
<t t-if="line.cashregister.journal.type !== 'bank'">
<t t-if="line.payment_method.is_cash_count">
<t t-esc='line.name' />
</t>
</t>
+6 -11
View File
@@ -67,18 +67,13 @@
</field>
</record>
<record id="view_account_journal_pos_user_form" model="ir.ui.view">
<field name="name">POS Journal</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="point_of_sale.view_account_journal_pos_user_form"></field>
<record id="pos_payment_method_view_form" model="ir.ui.view">
<field name="name">Payment Method</field>
<field name="model">pos.payment.method</field>
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"></field>
<field name="arch" type="xml">
<xpath expr="//page[@name='point_of_sale']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//page[@name='point_of_sale']" position="inside">
<group attrs="{'invisible': [('journal_user', '=', False)]}">
<field name="pos_mercury_config_id"/>
</group>
<xpath expr="//field[@name='company_id']" position="after">
<field name="pos_mercury_config_id" attrs="{'invisible': [('is_cash_count', '=', True)]}"/>
</xpath>
</field>
</record>