summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Caburnay <jcb@odoo.com>2019-08-08 08:32:30 +0000
committerPierre Masereel <pim@odoo.com>2019-08-09 13:57:11 +0000
commitb234e97f87c577a7cd85230c4f42277b39e8c842 (patch)
treed6eab8ae10ddc5efac943b5d8cd2d648bd73b9a4
parent536560bbf55d1d9a6f5bd8d8f145f7bb13569ae4 (diff)
[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
-rw-r--r--addons/l10n_fr_pos_cert/models/account_bank_statement.py4
-rw-r--r--addons/point_of_sale/__manifest__.py3
-rw-r--r--addons/point_of_sale/models/__init__.py2
-rw-r--r--addons/point_of_sale/models/account_journal.py17
-rw-r--r--addons/point_of_sale/models/pos_config.py51
-rw-r--r--addons/point_of_sale/models/pos_order.py496
-rw-r--r--addons/point_of_sale/models/pos_payment.py25
-rw-r--r--addons/point_of_sale/models/pos_payment_method.py58
-rw-r--r--addons/point_of_sale/models/pos_session.py614
-rw-r--r--addons/point_of_sale/models/res_company.py2
-rw-r--r--addons/point_of_sale/security/ir.model.access.csv3
-rw-r--r--addons/point_of_sale/security/point_of_sale_security.xml12
-rw-r--r--addons/point_of_sale/static/src/js/models.js93
-rw-r--r--addons/point_of_sale/static/src/js/screens.js16
-rw-r--r--addons/point_of_sale/static/src/xml/pos.xml7
-rw-r--r--addons/point_of_sale/views/account_journal_view.xml42
-rw-r--r--addons/point_of_sale/views/account_statement_view.xml3
-rw-r--r--addons/point_of_sale/views/point_of_sale_view.xml2
-rw-r--r--addons/point_of_sale/views/pos_config_view.xml10
-rw-r--r--addons/point_of_sale/views/pos_order_view.xml39
-rw-r--r--addons/point_of_sale/views/pos_payment_method_views.xml86
-rw-r--r--addons/point_of_sale/views/pos_payment_views.xml70
-rw-r--r--addons/point_of_sale/views/pos_session_view.xml33
-rw-r--r--addons/point_of_sale/wizard/__init__.py1
-rw-r--r--addons/point_of_sale/wizard/pos_open_statement.xml32
-rw-r--r--addons/point_of_sale/wizard/pos_payment.py55
-rw-r--r--addons/point_of_sale/wizard/pos_payment.xml4
-rw-r--r--addons/pos_mercury/models/pos_mercury.py50
-rw-r--r--addons/pos_mercury/models/pos_mercury_transaction.py12
-rw-r--r--addons/pos_mercury/static/src/js/pos_mercury.js61
-rw-r--r--addons/pos_mercury/static/src/xml/pos_mercury.xml4
-rw-r--r--addons/pos_mercury/views/pos_mercury_views.xml17
32 files changed, 1042 insertions, 882 deletions
diff --git a/addons/l10n_fr_pos_cert/models/account_bank_statement.py b/addons/l10n_fr_pos_cert/models/account_bank_statement.py
index 12344e1805a..f8ace6d2617 100644
--- a/addons/l10n_fr_pos_cert/models/account_bank_statement.py
+++ b/addons/l10n_fr_pos_cert/models/account_bank_statement.py
@@ -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()
diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py
index 46492ba0fbb..939c96c6a21 100644
--- a/addons/point_of_sale/__manifest__.py
+++ b/addons/point_of_sale/__manifest__.py
@@ -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',
diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py
index c7acfe82075..bde0f6f3819 100644
--- a/addons/point_of_sale/models/__init__.py
+++ b/addons/point_of_sale/models/__init__.py
@@ -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
diff --git a/addons/point_of_sale/models/account_journal.py b/addons/point_of_sale/models/account_journal.py
index dfb20ef43db..01371496185 100644
--- a/addons/point_of_sale/models/account_journal.py
+++ b/addons/point_of_sale/models/account_journal.py
@@ -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')
diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py
index bb550afe547..ab1ef162bdd 100644
--- a/addons/point_of_sale/models/pos_config.py
+++ b/addons/point_of_sale/models/pos_config.py
@@ -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):
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index ce992ea4fcf..4ace4e4f4f5 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -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):
diff --git a/addons/point_of_sale/models/pos_payment.py b/addons/point_of_sale/models/pos_payment.py
new file mode 100644
index 00000000000..e37e0d44396
--- /dev/null
+++ b/addons/point_of_sale/models/pos_payment.py
@@ -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')
diff --git a/addons/point_of_sale/models/pos_payment_method.py b/addons/point_of_sale/models/pos_payment_method.py
new file mode 100644
index 00000000000..be2197244c5
--- /dev/null
+++ b/addons/point_of_sale/models/pos_payment_method.py
@@ -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)
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 86e4661433b..a6885c3205d 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -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:
+ 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:
- self._validate_session()
+ 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()
diff --git a/addons/point_of_sale/models/res_company.py b/addons/point_of_sale/models/res_company.py
index 91a833ba7d6..64aa3646347 100644
--- a/addons/point_of_sale/models/res_company.py
+++ b/addons/point_of_sale/models/res_company.py
@@ -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)) \ No newline at end of file
+ raise ValidationError(_("Please close all the point of sale sessions in this period before closing it. Open sessions are: %s ") % (sessions_str))
diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv
index f92da922f39..76fe06de68d 100644
--- a/addons/point_of_sale/security/ir.model.access.csv
+++ b/addons/point_of_sale/security/ir.model.access.csv
@@ -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
diff --git a/addons/point_of_sale/security/point_of_sale_security.xml b/addons/point_of_sale/security/point_of_sale_security.xml
index 34366e90353..caf678d5705 100644
--- a/addons/point_of_sale/security/point_of_sale_security.xml
+++ b/addons/point_of_sale/security/point_of_sale_security.xml
@@ -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">
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index adbbded114a..ca290812484 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -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(){
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 377b93f4312..fc9649129ab 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -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',{
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index a1cf3607752..b7411ace036 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -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">
diff --git a/addons/point_of_sale/views/account_journal_view.xml b/addons/point_of_sale/views/account_journal_view.xml
index f0413ea112e..8a218447eb9 100644
--- a/addons/point_of_sale/views/account_journal_view.xml
+++ b/addons/point_of_sale/views/account_journal_view.xml
@@ -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 -->
diff --git a/addons/point_of_sale/views/account_statement_view.xml b/addons/point_of_sale/views/account_statement_view.xml
index f6975b0cc64..bc693d06aa6 100644
--- a/addons/point_of_sale/views/account_statement_view.xml
+++ b/addons/point_of_sale/views/account_statement_view.xml
@@ -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>
diff --git a/addons/point_of_sale/views/point_of_sale_view.xml b/addons/point_of_sale/views/point_of_sale_view.xml
index c157a3c4069..31d9db50398 100644
--- a/addons/point_of_sale/views/point_of_sale_view.xml
+++ b/addons/point_of_sale/views/point_of_sale_view.xml
@@ -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"
diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml
index 23809c269ff..fbed020f0fd 100644
--- a/addons/point_of_sale/views/pos_config_view.xml
+++ b/addons/point_of_sale/views/pos_config_view.xml
@@ -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>
diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml
index da6ddb7a4ee..1790a0dcf4e 100644
--- a/addons/point_of_sale/views/pos_order_view.xml
+++ b/addons/point_of_sale/views/pos_order_view.xml
@@ -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>
diff --git a/addons/point_of_sale/views/pos_payment_method_views.xml b/addons/point_of_sale/views/pos_payment_method_views.xml
new file mode 100644
index 00000000000..73d122a7a31
--- /dev/null
+++ b/addons/point_of_sale/views/pos_payment_method_views.xml
@@ -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>
diff --git a/addons/point_of_sale/views/pos_payment_views.xml b/addons/point_of_sale/views/pos_payment_views.xml
new file mode 100644
index 00000000000..aed337ca22f
--- /dev/null
+++ b/addons/point_of_sale/views/pos_payment_views.xml
@@ -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>
diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml
index 9ba67a2f1ae..2ef12e16b2e 100644
--- a/addons/point_of_sale/views/pos_session_view.xml
+++ b/addons/point_of_sale/views/pos_session_view.xml
@@ -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">
diff --git a/addons/point_of_sale/wizard/__init__.py b/addons/point_of_sale/wizard/__init__.py
index d85d8a81c86..16789e5d2ae 100644
--- a/addons/point_of_sale/wizard/__init__.py
+++ b/addons/point_of_sale/wizard/__init__.py
@@ -3,5 +3,4 @@
from . import pos_box
from . import pos_details
-from . import pos_open_statement
from . import pos_payment
diff --git a/addons/point_of_sale/wizard/pos_open_statement.xml b/addons/point_of_sale/wizard/pos_open_statement.xml
deleted file mode 100644
index 506a42667b3..00000000000
--- a/addons/point_of_sale/wizard/pos_open_statement.xml
+++ /dev/null
@@ -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>
diff --git a/addons/point_of_sale/wizard/pos_payment.py b/addons/point_of_sale/wizard/pos_payment.py
index 72998e7a3b6..f028ea8e83f 100644
--- a/addons/point_of_sale/wizard/pos_payment.py
+++ b/addons/point_of_sale/wizard/pos_payment.py
@@ -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 self.env['pos.order'].browse(active_id).session_id.config_id
return False
- def _default_journal(self):
+ def _default_amount(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
+ order = self.env['pos.order'].browse(active_id)
+ return order.amount_total - order.amount_paid
return False
- def _default_amount(self):
+ def _default_payment_method(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)
+ 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
- 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)
+ 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.Date(string='Payment Date', required=True, default=lambda self: fields.Date.context_today(self))
-
- @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)]}
- }
+ 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,
}
-
diff --git a/addons/point_of_sale/wizard/pos_payment.xml b/addons/point_of_sale/wizard/pos_payment.xml
index 2bdd45f3dbb..e44bb4293b5 100644
--- a/addons/point_of_sale/wizard/pos_payment.xml
+++ b/addons/point_of_sale/wizard/pos_payment.xml
@@ -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>
diff --git a/addons/pos_mercury/models/pos_mercury.py b/addons/pos_mercury/models/pos_mercury.py
index 4915f948afc..35e0e63bf72 100644
--- a/addons/pos_mercury/models/pos_mercury.py
+++ b/addons/pos_mercury/models/pos_mercury.py
@@ -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'
diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py
index a3ea29099f5..3b4473ee01c 100644
--- a/addons/pos_mercury/models/pos_mercury_transaction.py
+++ b/addons/pos_mercury/models/pos_mercury_transaction.py
@@ -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
diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js
index 9dbe66a5a8d..3e561e724cb 100644
--- a/addons/pos_mercury/static/src/js/pos_mercury.js
+++ b/addons/pos_mercury/static/src/js/pos_mercury.js
@@ -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 = [];
-
- $.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});
- }
- });
+ getOnlinePaymentMethods: function () {
+ var online_payment_methods = [];
- 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;
+ $.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 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));
}
},
diff --git a/addons/pos_mercury/static/src/xml/pos_mercury.xml b/addons/pos_mercury/static/src/xml/pos_mercury.xml
index a40973f7c30..aa52f7c0973 100644
--- a/addons/pos_mercury/static/src/xml/pos_mercury.xml
+++ b/addons/pos_mercury/static/src/xml/pos_mercury.xml
@@ -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>
diff --git a/addons/pos_mercury/views/pos_mercury_views.xml b/addons/pos_mercury/views/pos_mercury_views.xml
index 95530b34977..fb1c5495382 100644
--- a/addons/pos_mercury/views/pos_mercury_views.xml
+++ b/addons/pos_mercury/views/pos_mercury_views.xml
@@ -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>