mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 12:12:02 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account_bank_statement
|
||||
from . import account_cash_rounding
|
||||
from . import account_payment
|
||||
from . import account_journal
|
||||
from . import account_tax
|
||||
from . import account_move
|
||||
from . import barcode_rule
|
||||
from . import chart_template
|
||||
from . import digest
|
||||
from . import pos_category
|
||||
from . import pos_config
|
||||
from . import pos_order
|
||||
from . import pos_session
|
||||
from . import product
|
||||
from . import res_partner
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import stock_picking
|
||||
from . import stock_rule
|
||||
from . import stock_warehouse
|
||||
from . import pos_payment
|
||||
from . import pos_payment_method
|
||||
from . import pos_bill
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# Copyright (C) 2004-2008 PC Solutions (<http://pcsol.be>). All Rights Reserved
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_inherit = 'account.bank.statement.line'
|
||||
|
||||
pos_session_id = fields.Many2one('pos.session', string="Session", copy=False)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from odoo import api, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountCashRounding(models.Model):
|
||||
_inherit = 'account.cash.rounding'
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_pos_config(self):
|
||||
if self.env['pos.config'].search_count([('rounding_method', 'in', self.ids)], limit=1):
|
||||
raise UserError(_('You cannot delete a rounding method that is used in a Point of Sale configuration.'))
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# Copyright (C) 2004-2008 PC Solutions (<http://pcsol.be>). All Rights Reserved
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = 'account.journal'
|
||||
|
||||
pos_payment_method_ids = fields.One2many('pos.payment.method', 'journal_id', string='Point of Sale Payment Methods')
|
||||
|
||||
@api.constrains('type')
|
||||
def _check_type(self):
|
||||
methods = self.env['pos.payment.method'].sudo().search([("journal_id", "in", self.ids)])
|
||||
if methods:
|
||||
raise ValidationError(_("This journal is associated with a payment method. You cannot modify its type"))
|
||||
|
||||
def _check_no_active_payments(self):
|
||||
hanging_journal_entries = self.env['pos.payment'].search(
|
||||
[
|
||||
('payment_method_id', 'in', self.pos_payment_method_ids.ids),
|
||||
('session_id.state', '=', 'opened')
|
||||
], limit=1)
|
||||
if(hanging_journal_entries):
|
||||
payment_method = hanging_journal_entries.payment_method_id.name
|
||||
pos_order = hanging_journal_entries.pos_order_id.name
|
||||
pos_session = hanging_journal_entries.session_id.name
|
||||
raise ValidationError(_("This journal is associated with payment method %s that is being used by order %s in the active pos session %s", payment_method, pos_order, pos_session))
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_journal_except_with_active_payments(self):
|
||||
for journal in self:
|
||||
journal._check_no_active_payments()
|
||||
|
||||
def action_archive(self):
|
||||
self._check_no_active_payments()
|
||||
return super().action_archive()
|
||||
|
||||
def _get_journal_inbound_outstanding_payment_accounts(self):
|
||||
res = super()._get_journal_inbound_outstanding_payment_accounts()
|
||||
account_ids = set(res.ids)
|
||||
for payment_method in self.sudo().pos_payment_method_ids:
|
||||
account_ids.add(payment_method.outstanding_account_id.id or self.company_id.account_journal_payment_debit_account_id.id)
|
||||
return self.env['account.account'].browse(account_ids)
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
pos_order_ids = fields.One2many('pos.order', 'account_move')
|
||||
pos_payment_ids = fields.One2many('pos.payment', 'account_move_id')
|
||||
|
||||
@api.depends('tax_cash_basis_created_move_ids')
|
||||
def _compute_always_tax_exigible(self):
|
||||
super()._compute_always_tax_exigible()
|
||||
# The pos closing move does not create caba entries (anymore); we set the tax values directly on the closing move.
|
||||
# (But there may still be old closing moves that used caba entries from previous versions.)
|
||||
relevant_moves = self.filtered(lambda move: not (move.always_tax_exigible or move.tax_cash_basis_created_move_ids))
|
||||
if not relevant_moves:
|
||||
return
|
||||
sessions = self.env['pos.session'].with_context(active_test=False).search([
|
||||
('move_id', 'in', relevant_moves.ids),
|
||||
])
|
||||
sessions.move_id.always_tax_exigible = True
|
||||
|
||||
def _stock_account_get_last_step_stock_moves(self):
|
||||
stock_moves = super(AccountMove, self)._stock_account_get_last_step_stock_moves()
|
||||
for invoice in self.filtered(lambda x: x.move_type == 'out_invoice'):
|
||||
stock_moves += invoice.sudo().mapped('pos_order_ids.picking_ids.move_ids').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer')
|
||||
for invoice in self.filtered(lambda x: x.move_type == 'out_refund'):
|
||||
stock_moves += invoice.sudo().mapped('pos_order_ids.picking_ids.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'customer')
|
||||
return stock_moves
|
||||
|
||||
|
||||
def _get_invoiced_lot_values(self):
|
||||
self.ensure_one()
|
||||
|
||||
lot_values = super(AccountMove, self)._get_invoiced_lot_values()
|
||||
|
||||
if self.state == 'draft':
|
||||
return lot_values
|
||||
|
||||
# user may not have access to POS orders, but it's ok if they have
|
||||
# access to the invoice
|
||||
for order in self.sudo().pos_order_ids:
|
||||
for line in order.lines:
|
||||
lots = line.pack_lot_ids or False
|
||||
if lots:
|
||||
for lot in lots:
|
||||
lot_values.append({
|
||||
'product_name': lot.product_id.name,
|
||||
'quantity': line.qty if lot.product_id.tracking == 'lot' else 1.0,
|
||||
'uom_name': line.product_uom_id.name,
|
||||
'lot_name': lot.lot_name,
|
||||
'pos_lot_id': lot.id,
|
||||
})
|
||||
|
||||
return lot_values
|
||||
|
||||
def _compute_payments_widget_reconciled_info(self):
|
||||
"""Add pos_payment_name field in the reconciled vals to be able to show the payment method in the invoice."""
|
||||
super()._compute_payments_widget_reconciled_info()
|
||||
for move in self:
|
||||
if move.invoice_payments_widget:
|
||||
if move.state == 'posted' and move.is_invoice(include_receipts=True):
|
||||
reconciled_partials = move._get_all_reconciled_invoice_partials()
|
||||
for i, reconciled_partial in enumerate(reconciled_partials):
|
||||
counterpart_line = reconciled_partial['aml']
|
||||
pos_payment = counterpart_line.move_id.sudo().pos_payment_ids
|
||||
move.invoice_payments_widget['content'][i].update({
|
||||
'pos_payment_name': pos_payment.payment_method_id.name,
|
||||
})
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
def _stock_account_get_anglo_saxon_price_unit(self):
|
||||
self.ensure_one()
|
||||
if not self.product_id:
|
||||
return self.price_unit
|
||||
price_unit = super(AccountMoveLine, self)._stock_account_get_anglo_saxon_price_unit()
|
||||
sudo_order = self.move_id.sudo().pos_order_ids
|
||||
if sudo_order:
|
||||
price_unit = sudo_order._get_pos_anglo_saxon_price_unit(self.product_id, self.move_id.partner_id.id, self.quantity)
|
||||
return price_unit
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = 'account.payment'
|
||||
|
||||
pos_payment_method_id = fields.Many2one('pos.payment.method', "POS Payment Method")
|
||||
force_outstanding_account_id = fields.Many2one("account.account", "Forced Outstanding Account", check_company=True)
|
||||
pos_session_id = fields.Many2one('pos.session', "POS Session")
|
||||
|
||||
def _get_valid_liquidity_accounts(self):
|
||||
result = super()._get_valid_liquidity_accounts()
|
||||
return result | self.pos_payment_method_id.outstanding_account_id
|
||||
|
||||
@api.depends("force_outstanding_account_id")
|
||||
def _compute_outstanding_account_id(self):
|
||||
"""When force_outstanding_account_id is set, we use it as the outstanding_account_id."""
|
||||
super()._compute_outstanding_account_id()
|
||||
for payment in self:
|
||||
if payment.force_outstanding_account_id:
|
||||
payment.outstanding_account_id = payment.force_outstanding_account_id
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import split_every
|
||||
|
||||
|
||||
class AccountTax(models.Model):
|
||||
_inherit = 'account.tax'
|
||||
|
||||
def write(self, vals):
|
||||
forbidden_fields = {
|
||||
'amount_type', 'amount', 'type_tax_use', 'tax_group_id', 'price_include',
|
||||
'include_base_amount', 'is_base_affected',
|
||||
}
|
||||
if forbidden_fields & set(vals.keys()):
|
||||
lines = self.env['pos.order.line'].sudo().search([
|
||||
('order_id.session_id.state', '!=', 'closed')
|
||||
])
|
||||
self_ids = set(self.ids)
|
||||
for lines_chunk in map(self.env['pos.order.line'].sudo().browse, split_every(100000, lines.ids)):
|
||||
if any(tid in self_ids for ts in lines_chunk.read(['tax_ids']) for tid in ts['tax_ids']):
|
||||
raise UserError(_(
|
||||
'It is forbidden to modify a tax used in a POS order not posted. '
|
||||
'You must close the POS sessions before modifying the tax.'
|
||||
))
|
||||
lines_chunk.invalidate_cache(['tax_ids'], lines_chunk.ids)
|
||||
return super(AccountTax, self).write(vals)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class BarcodeRule(models.Model):
|
||||
_inherit = 'barcode.rule'
|
||||
|
||||
type = fields.Selection(selection_add=[
|
||||
('weight', 'Weighted Product'),
|
||||
('price', 'Priced Product'),
|
||||
('discount', 'Discounted Product'),
|
||||
('client', 'Client'),
|
||||
('cashier', 'Cashier')
|
||||
], ondelete={
|
||||
'weight': 'set default',
|
||||
'price': 'set default',
|
||||
'discount': 'set default',
|
||||
'client': 'set default',
|
||||
'cashier': 'set default',
|
||||
})
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class AccountChartTemplate(models.Model):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
def _load(self, company):
|
||||
"""Remove the payment methods that are created for the company before installing the chart of accounts.
|
||||
|
||||
Keeping these existing pos.payment.method records interferes with the installation of chart of accounts
|
||||
because pos.payment.method model has fields linked to account.journal and account.account records that are
|
||||
deleted during the loading of chart of accounts.
|
||||
"""
|
||||
self.env['pos.payment.method'].search([('company_id', '=', company.id)]).unlink()
|
||||
result = super(AccountChartTemplate, self)._load(company)
|
||||
self.env['pos.config'].post_install_pos_localisation(companies=company)
|
||||
return result
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class Digest(models.Model):
|
||||
_inherit = 'digest.digest'
|
||||
|
||||
kpi_pos_total = fields.Boolean('POS Sales')
|
||||
kpi_pos_total_value = fields.Monetary(compute='_compute_kpi_pos_total_value')
|
||||
|
||||
def _compute_kpi_pos_total_value(self):
|
||||
if not self.env.user.has_group('point_of_sale.group_pos_user'):
|
||||
raise AccessError(_("Do not have access, skip this data for user's digest email"))
|
||||
for record in self:
|
||||
start, end, company = record._get_kpi_compute_parameters()
|
||||
record.kpi_pos_total_value = sum(self.env['pos.order'].search([
|
||||
('date_order', '>=', start),
|
||||
('date_order', '<', end),
|
||||
('state', 'not in', ['draft', 'cancel', 'invoiced']),
|
||||
('company_id', '=', company.id)
|
||||
]).mapped('amount_total'))
|
||||
|
||||
def _compute_kpis_actions(self, company, user):
|
||||
res = super(Digest, self)._compute_kpis_actions(company, user)
|
||||
res['kpi_pos_total'] = 'point_of_sale.action_pos_sale_graph&menu_id=%s' % self.env.ref('point_of_sale.menu_point_root').id
|
||||
return res
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class Bill(models.Model):
|
||||
_name = "pos.bill"
|
||||
_order = "value"
|
||||
_description = "Coins/Bills"
|
||||
|
||||
name = fields.Char("Name")
|
||||
value = fields.Float("Coin/Bill Value", required=True, digits=(16, 4))
|
||||
pos_config_ids = fields.Many2many("pos.config", string="Point of Sales")
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
try:
|
||||
value = float(name)
|
||||
except:
|
||||
raise UserError(_("The name of the Coins/Bills must be a number."))
|
||||
result = super().create({"name": name, "value": value})
|
||||
return result.name_get()[0]
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
|
||||
class PosCategory(models.Model):
|
||||
_name = "pos.category"
|
||||
_description = "Point of Sale Category"
|
||||
_order = "sequence, name"
|
||||
|
||||
@api.constrains('parent_id')
|
||||
def _check_category_recursion(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(_('Error ! You cannot create recursive categories.'))
|
||||
|
||||
name = fields.Char(string='Category Name', required=True, translate=True)
|
||||
parent_id = fields.Many2one('pos.category', string='Parent Category', index=True)
|
||||
child_id = fields.One2many('pos.category', 'parent_id', string='Children Categories')
|
||||
sequence = fields.Integer(help="Gives the sequence order when displaying a list of product categories.")
|
||||
image_128 = fields.Image("Image", max_width=128, max_height=128)
|
||||
|
||||
# During loading of data, the image is not loaded so we expose a lighter
|
||||
# field to determine whether a pos.category has an image or not.
|
||||
has_image = fields.Boolean(compute='_compute_has_image')
|
||||
|
||||
def name_get(self):
|
||||
def get_names(cat):
|
||||
res = []
|
||||
while cat:
|
||||
res.append(cat.name)
|
||||
cat = cat.parent_id
|
||||
return res
|
||||
return [(cat.id, " / ".join(reversed(get_names(cat)))) for cat in self if cat.name]
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_session_open(self):
|
||||
if self.search_count([('id', 'in', self.ids)]):
|
||||
if self.env['pos.session'].sudo().search_count([('state', '!=', 'closed')]):
|
||||
raise UserError(_('You cannot delete a point of sale category while a session is still opened.'))
|
||||
|
||||
@api.depends('has_image')
|
||||
def _compute_has_image(self):
|
||||
for category in self:
|
||||
category.has_image = bool(category.image_128)
|
||||
|
|
@ -0,0 +1,709 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
import pytz
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import AccessError, ValidationError, UserError
|
||||
|
||||
|
||||
class PosConfig(models.Model):
|
||||
_name = 'pos.config'
|
||||
_description = 'Point of Sale Configuration'
|
||||
|
||||
def _default_warehouse_id(self):
|
||||
return self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).id
|
||||
|
||||
def _default_picking_type_id(self):
|
||||
return self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).pos_type_id.id
|
||||
|
||||
def _default_sale_journal(self):
|
||||
return self.env['account.journal'].search([('type', 'in', ('sale', 'general')), ('company_id', '=', self.env.company.id), ('code', '=', 'POSS')], limit=1)
|
||||
|
||||
def _default_invoice_journal(self):
|
||||
return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1)
|
||||
|
||||
def _default_payment_methods(self):
|
||||
""" Should only default to payment methods that are compatible to this config's company and currency.
|
||||
"""
|
||||
domain = [
|
||||
('split_transactions', '=', False),
|
||||
('company_id', '=', self.env.company.id),
|
||||
'|',
|
||||
('journal_id', '=', False),
|
||||
('journal_id.currency_id', 'in', (False, self.env.company.currency_id.id)),
|
||||
]
|
||||
non_cash_pm = self.env['pos.payment.method'].search(domain + [('is_cash_count', '=', False)])
|
||||
available_cash_pm = self.env['pos.payment.method'].search(domain + [('is_cash_count', '=', True),
|
||||
('config_ids', '=', False)], limit=1)
|
||||
return non_cash_pm | available_cash_pm
|
||||
|
||||
def _default_pricelist(self):
|
||||
return self.env['product.pricelist'].search([('company_id', 'in', (False, self.env.company.id)), ('currency_id', '=', self.env.company.currency_id.id)], limit=1)
|
||||
|
||||
def _get_group_pos_manager(self):
|
||||
return self.env.ref('point_of_sale.group_pos_manager')
|
||||
|
||||
def _get_group_pos_user(self):
|
||||
return self.env.ref('point_of_sale.group_pos_user')
|
||||
|
||||
name = fields.Char(string='Point of Sale', 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")
|
||||
picking_type_id = fields.Many2one(
|
||||
'stock.picking.type',
|
||||
string='Operation Type',
|
||||
default=_default_picking_type_id,
|
||||
required=True,
|
||||
domain="[('code', '=', 'outgoing'), ('warehouse_id.company_id', '=', company_id)]",
|
||||
ondelete='restrict')
|
||||
journal_id = fields.Many2one(
|
||||
'account.journal', string='Point of Sale Journal',
|
||||
domain=[('type', 'in', ('general', 'sale'))],
|
||||
help="Accounting journal used to post POS session journal entries and POS invoice payments.",
|
||||
default=_default_sale_journal,
|
||||
ondelete='restrict')
|
||||
invoice_journal_id = fields.Many2one(
|
||||
'account.journal', string='Invoice Journal',
|
||||
domain=[('type', '=', 'sale')],
|
||||
help="Accounting journal used to create invoices.",
|
||||
default=_default_invoice_journal)
|
||||
currency_id = fields.Many2one('res.currency', compute='_compute_currency', string="Currency")
|
||||
iface_cashdrawer = fields.Boolean(string='Cashdrawer', help="Automatically open the cashdrawer.")
|
||||
iface_electronic_scale = fields.Boolean(string='Electronic Scale', help="Enables Electronic Scale integration.")
|
||||
iface_customer_facing_display = fields.Boolean(compute='_compute_customer_facing_display')
|
||||
iface_customer_facing_display_via_proxy = fields.Boolean(string='Customer Facing Display', help="Show checkout to customers with a remotely-connected screen.")
|
||||
iface_customer_facing_display_local = fields.Boolean(string='Local Customer Facing Display', help="Show checkout to customers.")
|
||||
iface_print_via_proxy = fields.Boolean(string='Print via Proxy', help="Bypass browser printing and prints via the hardware proxy.")
|
||||
iface_scan_via_proxy = fields.Boolean(string='Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner and card swiping with a Vantiv card reader.")
|
||||
iface_big_scrollbars = fields.Boolean('Large Scrollbars', help='For imprecise industrial touchscreens.')
|
||||
iface_print_auto = fields.Boolean(string='Automatic Receipt Printing', default=False,
|
||||
help='The receipt will automatically be printed at the end of each order.')
|
||||
iface_print_skip_screen = fields.Boolean(string='Skip Preview Screen', default=True,
|
||||
help='The receipt screen will be skipped if the receipt can be printed automatically.')
|
||||
iface_tax_included = fields.Selection([('subtotal', 'Tax-Excluded Price'), ('total', 'Tax-Included Price')], string="Tax Display", default='total', required=True)
|
||||
iface_start_categ_id = fields.Many2one('pos.category', string='Initial Category',
|
||||
help='The point of sale will display this product category by default. If no category is specified, all available products will be shown.')
|
||||
iface_available_categ_ids = fields.Many2many('pos.category', string='Available PoS Product Categories',
|
||||
help='The point of sale will only display products which are within one of the selected category trees. If no category is specified, all available products will be shown')
|
||||
restrict_price_control = fields.Boolean(string='Restrict Price Modifications to Managers',
|
||||
help="Only users with Manager access rights for PoS app can modify the product prices on orders.")
|
||||
is_margins_costs_accessible_to_every_user = fields.Boolean(string='Margins & Costs', default=False,
|
||||
help='When disabled, only PoS manager can view the margin and cost of product among the Product info.')
|
||||
cash_control = fields.Boolean(string='Advanced Cash Control', compute='_compute_cash_control', help="Check the amount of the cashbox at opening and closing.")
|
||||
set_maximum_difference = fields.Boolean('Set Maximum Difference', help="Set a maximum difference allowed between the expected and counted money during the closing of the session.")
|
||||
receipt_header = fields.Text(string='Receipt Header', help="A short text that will be inserted as a header in the printed receipt.")
|
||||
receipt_footer = fields.Text(string='Receipt Footer', help="A short text that will be inserted as a footer in the printed receipt.")
|
||||
proxy_ip = fields.Char(string='IP Address', size=45,
|
||||
help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty.')
|
||||
active = fields.Boolean(default=True)
|
||||
uuid = fields.Char(readonly=True, default=lambda self: str(uuid4()), copy=False,
|
||||
help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.')
|
||||
sequence_id = fields.Many2one('ir.sequence', string='Order IDs Sequence', readonly=True,
|
||||
help="This sequence is automatically created by Odoo but you can change it "
|
||||
"to customize the reference numbers of your orders.", copy=False, ondelete='restrict')
|
||||
sequence_line_id = fields.Many2one('ir.sequence', string='Order Line IDs Sequence', readonly=True,
|
||||
help="This sequence is automatically created by Odoo but you can change it "
|
||||
"to customize the reference numbers of your orders lines.", copy=False)
|
||||
session_ids = fields.One2many('pos.session', 'config_id', string='Sessions')
|
||||
current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session")
|
||||
current_session_state = fields.Char(compute='_compute_current_session')
|
||||
number_of_opened_session = fields.Integer(string="Number of Opened Session", compute='_compute_current_session')
|
||||
last_session_closing_cash = fields.Float(compute='_compute_last_session')
|
||||
last_session_closing_date = fields.Date(compute='_compute_last_session')
|
||||
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')
|
||||
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,
|
||||
help="Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply.")
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
|
||||
group_pos_manager_id = fields.Many2one('res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager,
|
||||
help='This field is there to pass the id of the pos manager group to the point of sale client.')
|
||||
group_pos_user_id = fields.Many2one('res.groups', string='Point of Sale User Group', default=_get_group_pos_user,
|
||||
help='This field is there to pass the id of the pos user group to the point of sale client.')
|
||||
iface_tipproduct = fields.Boolean(string="Product tips")
|
||||
tip_product_id = fields.Many2one('product.product', string='Tip Product',
|
||||
help="This product is used as reference on customer receipts.")
|
||||
fiscal_position_ids = fields.Many2many('account.fiscal.position', string='Fiscal Positions', help='This is useful for restaurants with onsite and take-away services that imply specific tax rates.')
|
||||
default_fiscal_position_id = fields.Many2one('account.fiscal.position', string='Default Fiscal Position')
|
||||
default_bill_ids = fields.Many2many('pos.bill', string="Coins/Bills")
|
||||
use_pricelist = fields.Boolean("Use a pricelist.")
|
||||
tax_regime_selection = fields.Boolean("Tax Regime Selection value")
|
||||
start_category = fields.Boolean("Start Category", default=False)
|
||||
limit_categories = fields.Boolean("Restrict Categories")
|
||||
module_pos_restaurant = fields.Boolean("Is a Bar/Restaurant")
|
||||
module_pos_discount = fields.Boolean("Global Discounts")
|
||||
module_pos_mercury = fields.Boolean(string="Integrated Card Payments")
|
||||
is_posbox = fields.Boolean("PosBox")
|
||||
is_header_or_footer = fields.Boolean("Custom Header & Footer")
|
||||
module_pos_hr = fields.Boolean(help="Show employee login screen")
|
||||
amount_authorized_diff = fields.Float('Amount Authorized Difference',
|
||||
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())
|
||||
company_has_template = fields.Boolean(string="Company has chart of accounts", compute="_compute_company_has_template")
|
||||
current_user_id = fields.Many2one('res.users', string='Current Session Responsible', compute='_compute_current_session_user')
|
||||
other_devices = fields.Boolean(string="Other Devices", help="Connect devices to your PoS without an IoT Box.")
|
||||
rounding_method = fields.Many2one('account.cash.rounding', string="Cash rounding")
|
||||
cash_rounding = fields.Boolean(string="Cash Rounding")
|
||||
only_round_cash_method = fields.Boolean(string="Only apply rounding on cash")
|
||||
has_active_session = fields.Boolean(compute='_compute_current_session')
|
||||
manual_discount = fields.Boolean(string="Line Discounts", default=True)
|
||||
ship_later = fields.Boolean(string="Ship Later")
|
||||
warehouse_id = fields.Many2one('stock.warehouse', default=_default_warehouse_id, ondelete='restrict')
|
||||
route_id = fields.Many2one('stock.route', string="Spefic route for products delivered later.")
|
||||
picking_policy = fields.Selection([
|
||||
('direct', 'As soon as possible'),
|
||||
('one', 'When all products are ready')],
|
||||
string='Shipping Policy', required=True, default='direct',
|
||||
help="If you deliver all products at once, the delivery order will be scheduled based on the greatest "
|
||||
"product lead time. Otherwise, it will be based on the shortest.")
|
||||
limited_products_loading = fields.Boolean('Limited Product Loading',
|
||||
default=True,
|
||||
help="we load all starred products (favorite), all services, recent inventory movements of products, and the most recently updated products.\n"
|
||||
"When the session is open, we keep on loading all remaining products in the background.\n"
|
||||
"In the meantime, you can click on the 'database icon' in the searchbar to load products from database.")
|
||||
limited_products_amount = fields.Integer(default=20000)
|
||||
product_load_background = fields.Boolean(default=False)
|
||||
limited_partners_loading = fields.Boolean('Limited Partners Loading',
|
||||
default=True,
|
||||
help="By default, 10000 partners are loaded.\n"
|
||||
"When the session is open, we keep on loading all remaining partners in the background.\n"
|
||||
"In the meantime, you can use the 'Load Customers' button to load partners from database.")
|
||||
limited_partners_amount = fields.Integer(default=10000)
|
||||
partner_load_background = fields.Boolean(default=False)
|
||||
|
||||
@api.depends('payment_method_ids')
|
||||
def _compute_cash_control(self):
|
||||
for config in self:
|
||||
config.cash_control = bool(config.payment_method_ids.filtered('is_cash_count'))
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_company_has_template(self):
|
||||
for config in self:
|
||||
config.company_has_template = self.env['account.chart.template'].existing_accounting(config.company_id) or config.company_id.chart_template_id
|
||||
|
||||
def _compute_is_installed_account_accountant(self):
|
||||
account_accountant = self.env['ir.module.module'].sudo().search([('name', '=', 'account_accountant'), ('state', '=', 'installed')])
|
||||
for pos_config in self:
|
||||
pos_config.is_installed_account_accountant = account_accountant and account_accountant.id
|
||||
|
||||
@api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id', 'company_id', 'company_id.currency_id')
|
||||
def _compute_currency(self):
|
||||
for pos_config in self:
|
||||
if pos_config.journal_id:
|
||||
pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id
|
||||
else:
|
||||
pos_config.currency_id = pos_config.company_id.currency_id.id
|
||||
|
||||
@api.depends('session_ids', 'session_ids.state')
|
||||
def _compute_current_session(self):
|
||||
"""If there is an open session, store it to current_session_id / current_session_State.
|
||||
"""
|
||||
for pos_config in self:
|
||||
opened_sessions = pos_config.session_ids.filtered(lambda s: not s.state == 'closed')
|
||||
session = pos_config.session_ids.filtered(lambda s: not s.state == 'closed' and not s.rescue)
|
||||
# sessions ordered by id desc
|
||||
pos_config.number_of_opened_session = len(opened_sessions)
|
||||
pos_config.has_active_session = opened_sessions and True or False
|
||||
pos_config.current_session_id = session and session[0].id or False
|
||||
pos_config.current_session_state = session and session[0].state or False
|
||||
|
||||
@api.depends('session_ids')
|
||||
def _compute_last_session(self):
|
||||
PosSession = self.env['pos.session']
|
||||
for pos_config in self:
|
||||
session = PosSession.search_read(
|
||||
[('config_id', '=', pos_config.id), ('state', '=', 'closed')],
|
||||
['cash_register_balance_end_real', 'stop_at'],
|
||||
order="stop_at desc", limit=1)
|
||||
if session:
|
||||
timezone = pytz.timezone(self._context.get('tz') or self.env.user.tz or 'UTC')
|
||||
pos_config.last_session_closing_date = session[0]['stop_at'].astimezone(timezone).date()
|
||||
pos_config.last_session_closing_cash = session[0]['cash_register_balance_end_real']
|
||||
else:
|
||||
pos_config.last_session_closing_cash = 0
|
||||
pos_config.last_session_closing_date = False
|
||||
|
||||
@api.depends('session_ids')
|
||||
def _compute_current_session_user(self):
|
||||
for pos_config in self:
|
||||
session = pos_config.session_ids.filtered(lambda s: s.state in ['opening_control', 'opened', 'closing_control'] and not s.rescue)
|
||||
if session:
|
||||
pos_config.pos_session_username = session[0].user_id.sudo().name
|
||||
pos_config.pos_session_state = session[0].state
|
||||
pos_config.pos_session_duration = (
|
||||
datetime.now() - session[0].start_at
|
||||
).days if session[0].start_at else 0
|
||||
pos_config.current_user_id = session[0].user_id
|
||||
else:
|
||||
pos_config.pos_session_username = False
|
||||
pos_config.pos_session_state = False
|
||||
pos_config.pos_session_duration = 0
|
||||
pos_config.current_user_id = False
|
||||
|
||||
@api.depends('iface_customer_facing_display_via_proxy', 'iface_customer_facing_display_local')
|
||||
def _compute_customer_facing_display(self):
|
||||
for config in self:
|
||||
config.iface_customer_facing_display = config.iface_customer_facing_display_via_proxy or config.iface_customer_facing_display_local
|
||||
|
||||
@api.constrains('rounding_method')
|
||||
def _check_rounding_method_strategy(self):
|
||||
for config in self:
|
||||
if config.cash_rounding and config.rounding_method.strategy != 'add_invoice_line':
|
||||
selection_value = "Add a rounding line"
|
||||
for key, val in self.env["account.cash.rounding"]._fields["strategy"]._description_selection(config.env):
|
||||
if key == "add_invoice_line":
|
||||
selection_value = val
|
||||
break
|
||||
raise ValidationError(_(
|
||||
"The cash rounding strategy of the point of sale %(pos)s must be: '%(value)s'",
|
||||
pos=config.name,
|
||||
value=selection_value,
|
||||
))
|
||||
|
||||
@api.constrains('company_id', 'journal_id')
|
||||
def _check_company_journal(self):
|
||||
for config in self:
|
||||
if config.journal_id and config.journal_id.company_id.id != config.company_id.id:
|
||||
raise ValidationError(_("The sales journal of the point of sale %s must belong to its company.", config.name))
|
||||
|
||||
def _check_profit_loss_cash_journal(self):
|
||||
if self.cash_control and self.payment_method_ids:
|
||||
for method in self.payment_method_ids:
|
||||
if method.is_cash_count and (not method.journal_id.loss_account_id or not method.journal_id.profit_account_id):
|
||||
raise ValidationError(_("You need a loss and profit account on your cash journal."))
|
||||
|
||||
@api.constrains('company_id', 'invoice_journal_id')
|
||||
def _check_company_invoice_journal(self):
|
||||
for config in self:
|
||||
if config.invoice_journal_id and config.invoice_journal_id.company_id.id != config.company_id.id:
|
||||
raise ValidationError(_("The invoice journal of the point of sale %s must belong to the same company.", config.name))
|
||||
|
||||
@api.constrains('company_id', 'payment_method_ids')
|
||||
def _check_company_payment(self):
|
||||
for config in self:
|
||||
if self.env['pos.payment.method'].search_count([('id', 'in', config.payment_method_ids.ids), ('company_id', '!=', config.company_id.id)]):
|
||||
raise ValidationError(_("The payment methods for the point of sale %s must belong to its company.", self.name))
|
||||
|
||||
@api.constrains('pricelist_id', 'use_pricelist', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'payment_method_ids')
|
||||
def _check_currencies(self):
|
||||
for config in self:
|
||||
if config.use_pricelist and config.pricelist_id not in config.available_pricelist_ids:
|
||||
raise ValidationError(_("The default pricelist must be included in the available pricelists."))
|
||||
|
||||
# Check if the config's payment methods are compatible with its currency
|
||||
for pm in config.payment_method_ids:
|
||||
if pm.journal_id and pm.journal_id.currency_id and pm.journal_id.currency_id != config.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."))
|
||||
|
||||
if any(self.available_pricelist_ids.mapped(lambda pricelist: pricelist.currency_id != self.currency_id)):
|
||||
raise ValidationError(_("All available pricelists must be in the same currency as the company or"
|
||||
" as the Sales Journal set on this point of sale if you use"
|
||||
" 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."))
|
||||
|
||||
@api.constrains('iface_start_categ_id', 'iface_available_categ_ids')
|
||||
def _check_start_categ(self):
|
||||
for config in self:
|
||||
allowed_categ_ids = config.iface_available_categ_ids or self.env['pos.category'].search([])
|
||||
if config.iface_start_categ_id and config.iface_start_categ_id not in allowed_categ_ids:
|
||||
raise ValidationError(_("Start category should belong in the available categories."))
|
||||
|
||||
def _check_payment_method_ids(self):
|
||||
self.ensure_one()
|
||||
if not self.payment_method_ids:
|
||||
raise ValidationError(
|
||||
_("You must have at least one payment method configured to launch a session.")
|
||||
)
|
||||
|
||||
@api.constrains('limited_partners_amount', 'limited_partners_loading')
|
||||
def _check_limited_partners(self):
|
||||
for rec in self:
|
||||
if rec.limited_partners_loading and not rec.limited_partners_amount:
|
||||
raise ValidationError(
|
||||
_("Number of partners loaded can not be 0"))
|
||||
|
||||
@api.constrains('limited_products_amount', 'limited_products_loading')
|
||||
def _check_limited_products(self):
|
||||
for rec in self:
|
||||
if rec.limited_products_loading and not rec.limited_products_amount:
|
||||
raise ValidationError(
|
||||
_("Number of product loaded can not be 0"))
|
||||
|
||||
@api.constrains('pricelist_id', 'available_pricelist_ids')
|
||||
def _check_pricelists(self):
|
||||
self._check_companies()
|
||||
self = self.sudo()
|
||||
if self.pricelist_id.company_id and self.pricelist_id.company_id != self.company_id:
|
||||
raise ValidationError(
|
||||
_("The default pricelist must belong to no company or the company of the point of sale."))
|
||||
|
||||
@api.constrains('company_id', 'available_pricelist_ids')
|
||||
def _check_companies(self):
|
||||
for config in self:
|
||||
if any(pricelist.company_id.id not in [False, config.company_id.id] for pricelist in config.available_pricelist_ids):
|
||||
raise ValidationError(_("The selected pricelists must belong to no company or the company of the point of sale."))
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for config in self:
|
||||
last_session = self.env['pos.session'].search([('config_id', '=', config.id)], limit=1)
|
||||
if (not last_session) or (last_session.state == 'closed'):
|
||||
result.append((config.id, _("%(pos_name)s (not used)", pos_name=config.name)))
|
||||
else:
|
||||
result.append((config.id, "%s (%s)" % (config.name, last_session.user_id.name)))
|
||||
return result
|
||||
|
||||
def _check_header_footer(self, values):
|
||||
if not self.env.is_admin() and {'is_header_or_footer', 'receipt_header', 'receipt_footer'} & values.keys():
|
||||
raise AccessError(_('Only administrators can edit receipt headers and footers'))
|
||||
|
||||
def _config_sequence_implementation(self):
|
||||
return 'standard'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
self._check_header_footer(vals)
|
||||
IrSequence = self.env['ir.sequence'].sudo()
|
||||
val = {
|
||||
'name': _('POS Order %s', vals['name']),
|
||||
'padding': 4,
|
||||
'prefix': "%s/" % vals['name'],
|
||||
'code': "pos.order",
|
||||
'company_id': vals.get('company_id', False),
|
||||
'implementation': self._config_sequence_implementation(),
|
||||
}
|
||||
# force sequence_id field to new pos.order sequence
|
||||
vals['sequence_id'] = IrSequence.create(val).id
|
||||
|
||||
val.update(name=_('POS order line %s', vals['name']), code='pos.order.line')
|
||||
vals['sequence_line_id'] = IrSequence.create(val).id
|
||||
pos_configs = super().create(vals_list)
|
||||
pos_configs.sudo()._check_modules_to_install()
|
||||
pos_configs.sudo()._check_groups_implied()
|
||||
# If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
|
||||
return pos_configs
|
||||
|
||||
def _reset_default_on_vals(self, vals):
|
||||
if 'tip_product_id' in vals and any(self.mapped('iface_tipproduct')) and not vals['tip_product_id']:
|
||||
default_product = self.env.ref('point_of_sale.product_product_tip', False)
|
||||
if default_product:
|
||||
vals['tip_product_id'] = default_product.id
|
||||
else:
|
||||
raise UserError(_('The default tip product is missing. Please manually specify the tip product. (See Tips field.)'))
|
||||
|
||||
def write(self, vals):
|
||||
self._check_header_footer(vals)
|
||||
self._reset_default_on_vals(vals)
|
||||
opened_session = self.mapped('session_ids').filtered(lambda s: s.state != 'closed')
|
||||
if opened_session:
|
||||
forbidden_fields = []
|
||||
for key in self._get_forbidden_change_fields():
|
||||
if key in vals.keys():
|
||||
field_name = self._fields[key].get_description(self.env)["string"]
|
||||
forbidden_fields.append(field_name)
|
||||
if len(forbidden_fields) > 0:
|
||||
raise UserError(_(
|
||||
"Unable to modify this PoS Configuration because you can't modify %s while a session is open.",
|
||||
", ".join(forbidden_fields)
|
||||
))
|
||||
result = super(PosConfig, self).write(vals)
|
||||
|
||||
self.sudo()._set_fiscal_position()
|
||||
self.sudo()._check_modules_to_install()
|
||||
self.sudo()._check_groups_implied()
|
||||
return result
|
||||
|
||||
def _get_forbidden_change_fields(self):
|
||||
forbidden_keys = ['module_pos_hr', 'module_pos_restaurant', 'available_pricelist_ids',
|
||||
'limit_categories', 'iface_available_categ_ids', 'use_pricelist', 'module_pos_discount',
|
||||
'payment_method_ids', 'iface_tipproduc']
|
||||
return forbidden_keys
|
||||
|
||||
def unlink(self):
|
||||
# Delete the pos.config records first then delete the sequences linked to them
|
||||
sequences_to_delete = self.sequence_id | self.sequence_line_id
|
||||
res = super(PosConfig, self).unlink()
|
||||
sequences_to_delete.unlink()
|
||||
return res
|
||||
|
||||
# TODO-JCB: Maybe we can move this logic in `_reset_default_on_vals`
|
||||
def _set_fiscal_position(self):
|
||||
for config in self:
|
||||
if config.tax_regime_selection and config.default_fiscal_position_id and (config.default_fiscal_position_id.id not in config.fiscal_position_ids.ids):
|
||||
config.fiscal_position_ids = [(4, config.default_fiscal_position_id.id)]
|
||||
elif not config.tax_regime_selection and config.fiscal_position_ids.ids:
|
||||
config.fiscal_position_ids = [(5, 0, 0)]
|
||||
|
||||
def _check_modules_to_install(self):
|
||||
# determine modules to install
|
||||
expected = [
|
||||
fname[7:] # 'module_account' -> 'account'
|
||||
for fname in self._fields
|
||||
if fname.startswith('module_')
|
||||
if any(pos_config[fname] for pos_config in self)
|
||||
]
|
||||
if expected:
|
||||
STATES = ('installed', 'to install', 'to upgrade')
|
||||
modules = self.env['ir.module.module'].sudo().search([('name', 'in', expected)])
|
||||
modules = modules.filtered(lambda module: module.state not in STATES)
|
||||
if modules:
|
||||
modules.button_immediate_install()
|
||||
# just in case we want to do something if we install a module. (like a refresh ...)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_groups_implied(self):
|
||||
for pos_config in self:
|
||||
for field_name in [f for f in pos_config._fields if f.startswith('group_')]:
|
||||
field = pos_config._fields[field_name]
|
||||
if field.type in ('boolean', 'selection') and hasattr(field, 'implied_group'):
|
||||
field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',')
|
||||
field_groups = self.env['res.groups'].concat(*(self.env.ref(it) for it in field_group_xmlids))
|
||||
field_groups.write({'implied_ids': [(4, self.env.ref(field.implied_group).id)]})
|
||||
|
||||
|
||||
def execute(self):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
'params': {'wait': True}
|
||||
}
|
||||
|
||||
def _force_http(self):
|
||||
enforce_https = self.env['ir.config_parameter'].sudo().get_param('point_of_sale.enforce_https')
|
||||
if not enforce_https and self.other_devices:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Methods to open the POS
|
||||
def _action_to_open_ui(self):
|
||||
if not self.current_session_id:
|
||||
self.env['pos.session'].create({'user_id': self.env.uid, 'config_id': self.id})
|
||||
path = '/pos/web' if self._force_http() else '/pos/ui'
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': path + '?config_id=%d' % self.id,
|
||||
'target': 'self',
|
||||
}
|
||||
|
||||
def _check_before_creating_new_session(self):
|
||||
self._check_pricelists()
|
||||
self._check_company_journal()
|
||||
self._check_company_invoice_journal()
|
||||
self._check_company_payment()
|
||||
self._check_currencies()
|
||||
self._check_profit_loss_cash_journal()
|
||||
self._check_payment_method_ids()
|
||||
|
||||
def open_ui(self):
|
||||
"""Open the pos interface with config_id as an extra argument.
|
||||
|
||||
In vanilla PoS each user can only have one active session, therefore it was not needed to pass the config_id
|
||||
on opening a session. It is also possible to login to sessions created by other users.
|
||||
|
||||
:returns: dict
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.current_session_id:
|
||||
self._check_before_creating_new_session()
|
||||
self._validate_fields(self._fields)
|
||||
|
||||
# check if there's any product for this PoS
|
||||
domain = [('available_in_pos', '=', True)]
|
||||
if self.limit_categories and self.iface_available_categ_ids:
|
||||
domain.append(('pos_categ_id', 'in', self.iface_available_categ_ids.ids))
|
||||
if not self.env['product.product'].search(domain):
|
||||
return {
|
||||
'name': _("There is no product linked to your PoS"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'pos.session.check_product_wizard',
|
||||
'target': 'new',
|
||||
'context': {'config_id': self.id}
|
||||
}
|
||||
|
||||
return self._action_to_open_ui()
|
||||
|
||||
def open_existing_session_cb(self):
|
||||
""" close session button
|
||||
|
||||
access session form to validate entries
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self._open_session(self.current_session_id.id)
|
||||
|
||||
def _open_session(self, session_id):
|
||||
self._check_pricelists() # The pricelist company might have changed after the first opening of the session
|
||||
return {
|
||||
'name': _('Session'),
|
||||
'view_mode': 'form,tree',
|
||||
'res_model': 'pos.session',
|
||||
'res_id': session_id,
|
||||
'view_id': False,
|
||||
'type': 'ir.actions.act_window',
|
||||
}
|
||||
|
||||
def open_opened_session_list(self):
|
||||
return {
|
||||
'name': _('Opened Sessions'),
|
||||
'res_model': 'pos.session',
|
||||
'view_mode': 'tree,kanban,form',
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('state', '!=', 'closed'), ('config_id', '=', self.id)]
|
||||
}
|
||||
|
||||
# All following methods are made to create data needed in POS, when a localisation
|
||||
# is installed, or if POS is installed on database having companies that already have
|
||||
# a localisation installed
|
||||
@api.model
|
||||
def post_install_pos_localisation(self, companies=False):
|
||||
self = self.sudo()
|
||||
if not companies:
|
||||
companies = self.env['res.company'].search([])
|
||||
for company in companies.filtered('chart_template_id'):
|
||||
pos_configs = self.search([('company_id', '=', company.id)])
|
||||
pos_configs.setup_defaults(company)
|
||||
|
||||
def setup_defaults(self, company):
|
||||
"""Extend this method to customize the existing pos.config of the company during the installation
|
||||
of a localisation.
|
||||
|
||||
:param self pos.config: pos.config records present in the company during the installation of localisation.
|
||||
:param company res.company: the single company where the pos.config defaults will be setup.
|
||||
"""
|
||||
self.assign_payment_journals(company)
|
||||
self.generate_pos_journal(company)
|
||||
self.setup_invoice_journal(company)
|
||||
|
||||
def assign_payment_journals(self, company):
|
||||
for pos_config in self:
|
||||
if pos_config.payment_method_ids or pos_config.has_active_session:
|
||||
continue
|
||||
cash_journal = self.env['account.journal'].search([
|
||||
('company_id', '=', company.id),
|
||||
('type', '=', 'cash'),
|
||||
('currency_id', 'in', [pos_config.currency_id.id, False]),
|
||||
], limit=1)
|
||||
bank_journal = self.env['account.journal'].search([
|
||||
('company_id', '=', company.id),
|
||||
('type', '=', 'bank'),
|
||||
('currency_id', 'in', [pos_config.currency_id.id, False]),
|
||||
], limit=1)
|
||||
payment_methods = self.env['pos.payment.method']
|
||||
if cash_journal:
|
||||
payment_methods |= payment_methods.create({
|
||||
'name': _('Cash'),
|
||||
'journal_id': cash_journal.id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
if bank_journal:
|
||||
payment_methods |= payment_methods.create({
|
||||
'name': _('Bank'),
|
||||
'journal_id': bank_journal.id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
payment_methods |= payment_methods.create({
|
||||
'name': _('Customer Account'),
|
||||
'company_id': company.id,
|
||||
'split_transactions': True,
|
||||
})
|
||||
pos_config.write({'payment_method_ids': [(6, 0, payment_methods.ids)]})
|
||||
|
||||
def generate_pos_journal(self, company):
|
||||
for pos_config in self:
|
||||
if pos_config.journal_id:
|
||||
continue
|
||||
pos_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'POSS')])
|
||||
if not pos_journal:
|
||||
pos_journal = self.env['account.journal'].create({
|
||||
'type': 'general',
|
||||
'name': _('Point of Sale'),
|
||||
'code': 'POSS',
|
||||
'company_id': company.id,
|
||||
'sequence': 20
|
||||
})
|
||||
pos_config.write({'journal_id': pos_journal.id})
|
||||
|
||||
def setup_invoice_journal(self, company):
|
||||
for pos_config in self:
|
||||
invoice_journal_id = pos_config.invoice_journal_id or self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', company.id)], limit=1)
|
||||
if invoice_journal_id:
|
||||
pos_config.write({'invoice_journal_id': invoice_journal_id.id})
|
||||
|
||||
def get_limited_products_loading(self, fields):
|
||||
query = """
|
||||
WITH pm AS (
|
||||
SELECT product_id,
|
||||
Max(write_date) date
|
||||
FROM stock_move_line
|
||||
GROUP BY product_id
|
||||
ORDER BY date DESC
|
||||
)
|
||||
SELECT p.id
|
||||
FROM product_product p
|
||||
LEFT JOIN product_template t ON product_tmpl_id=t.id
|
||||
LEFT JOIN pm ON p.id=pm.product_id
|
||||
WHERE (
|
||||
t.available_in_pos
|
||||
AND t.sale_ok
|
||||
AND (t.company_id=%(company_id)s OR t.company_id IS NULL)
|
||||
AND %(available_categ_ids)s IS NULL OR t.pos_categ_id=ANY(%(available_categ_ids)s)
|
||||
) OR p.id=%(tip_product_id)s
|
||||
ORDER BY t.priority DESC,
|
||||
case when t.detailed_type = 'service' then 1 else 0 end DESC,
|
||||
pm.date DESC NULLS LAST,
|
||||
p.write_date
|
||||
LIMIT %(limit)s
|
||||
"""
|
||||
params = {
|
||||
'company_id': self.company_id.id,
|
||||
'available_categ_ids': self.iface_available_categ_ids.mapped('id') if self.iface_available_categ_ids else None,
|
||||
'tip_product_id': self.tip_product_id.id if self.tip_product_id else None,
|
||||
'limit': self.limited_products_amount
|
||||
}
|
||||
self.env.cr.execute(query, params)
|
||||
product_ids = self.env.cr.fetchall()
|
||||
products = self.env['product.product'].search_read([('id', 'in', product_ids)], fields=fields)
|
||||
return products
|
||||
|
||||
def get_limited_partners_loading(self):
|
||||
self.env.cr.execute("""
|
||||
WITH pm AS
|
||||
(
|
||||
SELECT partner_id,
|
||||
Count(partner_id) order_count
|
||||
FROM pos_order
|
||||
GROUP BY partner_id)
|
||||
SELECT id
|
||||
FROM res_partner AS partner
|
||||
LEFT JOIN pm
|
||||
ON (
|
||||
partner.id = pm.partner_id)
|
||||
WHERE (
|
||||
partner.company_id=%s OR partner.company_id IS NULL
|
||||
)
|
||||
ORDER BY COALESCE(pm.order_count, 0) DESC,
|
||||
NAME limit %s;
|
||||
""", [self.company_id.id, str(self.limited_partners_amount)])
|
||||
result = self.env.cr.fetchall()
|
||||
return result
|
||||
|
||||
def action_pos_config_modal_edit(self):
|
||||
return {
|
||||
'view_mode': 'form',
|
||||
'res_model': 'pos.config',
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'res_id': self.id,
|
||||
'context': {'pos_config_open_modal': True},
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,147 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.tools import formatLang, float_is_zero
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
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, index=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', store=True, index=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', related='pos_order_id.company_id', store=True)
|
||||
card_type = fields.Char('Type of card used')
|
||||
cardholder_name = fields.Char('Cardholder Name')
|
||||
transaction_id = fields.Char('Payment Transaction ID')
|
||||
payment_status = fields.Char('Payment Status')
|
||||
ticket = fields.Char('Payment Receipt Info')
|
||||
is_change = fields.Boolean(string='Is this payment change?', default=False)
|
||||
account_move_id = fields.Many2one('account.move', index='btree_not_null')
|
||||
|
||||
def name_get(self):
|
||||
res = []
|
||||
for payment in self:
|
||||
if payment.name:
|
||||
res.append((payment.id, '%s %s' % (payment.name, formatLang(self.env, payment.amount, currency_obj=payment.currency_id))))
|
||||
else:
|
||||
res.append((payment.id, formatLang(self.env, payment.amount, currency_obj=payment.currency_id)))
|
||||
return res
|
||||
|
||||
@api.constrains('payment_method_id')
|
||||
def _check_payment_method_id(self):
|
||||
for payment in self:
|
||||
if payment.payment_method_id not in payment.session_id.config_id.payment_method_ids:
|
||||
raise ValidationError(_('The payment method selected is not allowed in the config of the POS session.'))
|
||||
|
||||
def _export_for_ui(self, payment):
|
||||
return {
|
||||
'payment_method_id': payment.payment_method_id.id,
|
||||
'amount': payment.amount,
|
||||
'payment_status': payment.payment_status,
|
||||
'card_type': payment.card_type,
|
||||
'cardholder_name': payment.cardholder_name,
|
||||
'transaction_id': payment.transaction_id,
|
||||
'ticket': payment.ticket,
|
||||
'is_change': payment.is_change,
|
||||
}
|
||||
|
||||
def export_for_ui(self):
|
||||
return self.mapped(self._export_for_ui) if self else []
|
||||
|
||||
def _create_payment_moves(self, is_reverse=False):
|
||||
result = self.env['account.move']
|
||||
change_payment = self.filtered(lambda p: p.is_change and p.payment_method_id.type == 'cash')
|
||||
payment_to_change = self.filtered(lambda p: not p.is_change and p.payment_method_id.type == 'cash')[:1]
|
||||
normal_payments = (self - payment_to_change) - change_payment if change_payment else self
|
||||
|
||||
# Handle normal payments
|
||||
for payment in normal_payments:
|
||||
payment_method = payment.payment_method_id
|
||||
if payment_method.type == 'pay_later' or float_is_zero(payment.amount, precision_rounding=payment.pos_order_id.currency_id.rounding):
|
||||
continue
|
||||
payment_move = payment._create_payment_move_entry(is_reverse)
|
||||
payment.write({'account_move_id': payment_move.id})
|
||||
result |= payment_move
|
||||
payment_move._post()
|
||||
|
||||
# Handle change payments
|
||||
if change_payment and payment_to_change:
|
||||
result |= payment_to_change._create_payment_move_with_change(is_reverse, change_payment)
|
||||
|
||||
return result
|
||||
|
||||
def _create_payment_move_with_change(self, is_reverse, change_payment):
|
||||
if self.payment_method_id.type != 'pay_later' and not float_is_zero(self.amount, precision_rounding=self.pos_order_id.currency_id.rounding):
|
||||
payment_move = self._generate_payment_move(is_reverse, change_payment)
|
||||
self.write({'account_move_id': payment_move.id})
|
||||
payment_move._post()
|
||||
return payment_move
|
||||
|
||||
def _create_payment_move_entry(self, is_reverse=False):
|
||||
self.ensure_one()
|
||||
return self._generate_payment_move(is_reverse)
|
||||
|
||||
def _generate_payment_move(self, is_reverse, change_payment=None):
|
||||
order = self.pos_order_id
|
||||
pos_session = order.session_id
|
||||
journal = pos_session.config_id.journal_id
|
||||
pos_payment_ids = self.ids
|
||||
payment_amount = self.amount
|
||||
|
||||
if change_payment:
|
||||
pos_payment_ids += change_payment.ids
|
||||
payment_amount += change_payment.amount
|
||||
|
||||
payment_move = self.env['account.move'].with_context(default_journal_id=journal.id).create({
|
||||
'journal_id': journal.id,
|
||||
'date': fields.Date.context_today(order, order.date_order),
|
||||
'ref': _('Invoice payment for %s (%s) using %s') % (order.name, order.account_move.name, self.payment_method_id.name),
|
||||
'pos_payment_ids': pos_payment_ids,
|
||||
})
|
||||
amounts = pos_session._update_amounts({'amount': 0, 'amount_converted': 0}, {'amount': payment_amount}, self.payment_date)
|
||||
credit_line_values = self._prepare_credit_line_payment(payment_move)
|
||||
credit_line_vals = pos_session._credit_amounts(credit_line_values, amounts['amount'], amounts['amount_converted'])
|
||||
debit_line_values = self._prepare_debit_line_payment(payment_move, is_reverse)
|
||||
debit_line_vals = pos_session._debit_amounts(debit_line_values, amounts['amount'], amounts['amount_converted'])
|
||||
self.env['account.move.line'].with_context(check_move_validity=False).create([credit_line_vals, debit_line_vals])
|
||||
return payment_move
|
||||
|
||||
def _prepare_credit_line_payment(self, payment_move):
|
||||
accounting_partner = self.env["res.partner"]._find_accounting_partner(self.partner_id)
|
||||
order = self.pos_order_id
|
||||
return {
|
||||
'account_id': accounting_partner.with_company(order.company_id).property_account_receivable_id.id, # The field being company dependant, we need to make sure the right value is received.
|
||||
'move_id': payment_move.id,
|
||||
'partner_id': accounting_partner.id,
|
||||
}
|
||||
|
||||
def _prepare_debit_line_payment(self, payment_move, is_reverse):
|
||||
accounting_partner = self.env["res.partner"]._find_accounting_partner(self.partner_id)
|
||||
order = self.pos_order_id
|
||||
is_split_transaction = self.payment_method_id.split_transactions
|
||||
if is_split_transaction and is_reverse:
|
||||
reversed_move_receivable_account_id = accounting_partner.with_company(order.company_id).property_account_receivable_id.id
|
||||
elif is_reverse:
|
||||
reversed_move_receivable_account_id = self.payment_method_id.receivable_account_id.id or self.company_id.account_default_pos_receivable_account_id.id
|
||||
else:
|
||||
reversed_move_receivable_account_id = self.company_id.account_default_pos_receivable_account_id.id
|
||||
return {
|
||||
'account_id': reversed_move_receivable_account_id,
|
||||
'move_id': payment_move.id,
|
||||
'partner_id': accounting_partner.id if is_split_transaction and is_reverse else False,
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class PosPaymentMethod(models.Model):
|
||||
_name = "pos.payment.method"
|
||||
_description = "Point of Sale Payment Methods"
|
||||
_order = "id asc"
|
||||
|
||||
def _get_payment_terminal_selection(self):
|
||||
return []
|
||||
|
||||
name = fields.Char(string="Method", required=True, translate=True, help='Defines the name of the payment method that will be displayed in the Point of Sale when the payments are selected.')
|
||||
outstanding_account_id = fields.Many2one('account.account',
|
||||
string='Outstanding Account',
|
||||
ondelete='restrict',
|
||||
help='Leave empty to use the default account from the company setting.\n'
|
||||
'Account used as outstanding account when creating accounting payment records for bank payments.')
|
||||
receivable_account_id = fields.Many2one('account.account',
|
||||
string='Intermediary Account',
|
||||
ondelete='restrict',
|
||||
domain=[('reconcile', '=', True), ('account_type', '=', 'asset_receivable')],
|
||||
help="Leave empty to use the default account from the company setting.\n"
|
||||
"Overrides the company's receivable account (for Point of Sale) used in the journal entries.")
|
||||
is_cash_count = fields.Boolean(string='Cash', compute="_compute_is_cash_count", store=True)
|
||||
journal_id = fields.Many2one('account.journal',
|
||||
string='Journal',
|
||||
domain=[('type', 'in', ('cash', 'bank'))],
|
||||
ondelete='restrict',
|
||||
help='Leave empty to use the receivable account of customer.\n'
|
||||
'Defines the journal where to book the accumulated payments (or individual payment if Identify Customer is true) after closing the session.\n'
|
||||
'For cash journal, we directly write to the default account in the journal via statement lines.\n'
|
||||
'For bank journal, we write to the outstanding account specified in this payment method.\n'
|
||||
'Only cash and bank journals are allowed.')
|
||||
split_transactions = fields.Boolean(
|
||||
string='Identify Customer',
|
||||
default=False,
|
||||
help='Forces to set a customer when using this payment method and splits the journal entries for each customer. It could slow down the closing process.')
|
||||
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)
|
||||
use_payment_terminal = fields.Selection(selection=lambda self: self._get_payment_terminal_selection(), string='Use a Payment Terminal', help='Record payments with a terminal on this journal.')
|
||||
# used to hide use_payment_terminal when no payment interfaces are installed
|
||||
hide_use_payment_terminal = fields.Boolean(compute='_compute_hide_use_payment_terminal')
|
||||
active = fields.Boolean(default=True)
|
||||
type = fields.Selection(selection=[('cash', 'Cash'), ('bank', 'Bank'), ('pay_later', 'Customer Account')], compute="_compute_type")
|
||||
|
||||
@api.depends('type')
|
||||
def _compute_hide_use_payment_terminal(self):
|
||||
no_terminals = not bool(self._fields['use_payment_terminal'].selection(self))
|
||||
for payment_method in self:
|
||||
payment_method.hide_use_payment_terminal = no_terminals or payment_method.type in ('cash', 'pay_later')
|
||||
|
||||
@api.onchange('use_payment_terminal')
|
||||
def _onchange_use_payment_terminal(self):
|
||||
"""Used by inheriting model to unset the value of the field related to the unselected payment terminal."""
|
||||
pass
|
||||
|
||||
@api.depends('config_ids')
|
||||
def _compute_open_session_ids(self):
|
||||
for payment_method in self:
|
||||
payment_method.open_session_ids = self.env['pos.session'].search([('config_id', 'in', payment_method.config_ids.ids), ('state', '!=', 'closed')])
|
||||
|
||||
@api.depends('journal_id', 'split_transactions')
|
||||
def _compute_type(self):
|
||||
for pm in self:
|
||||
if pm.journal_id.type in {'cash', 'bank'}:
|
||||
pm.type = pm.journal_id.type
|
||||
else:
|
||||
pm.type = 'pay_later'
|
||||
|
||||
@api.onchange('journal_id')
|
||||
def _onchange_journal_id(self):
|
||||
for pm in self:
|
||||
if pm.journal_id and pm.journal_id.type not in ['cash', 'bank']:
|
||||
raise UserError(_("Only journals of type 'Cash' or 'Bank' could be used with payment methods."))
|
||||
if self.is_cash_count:
|
||||
self.use_payment_terminal = False
|
||||
|
||||
@api.depends('type')
|
||||
def _compute_is_cash_count(self):
|
||||
for pm in self:
|
||||
pm.is_cash_count = pm.type == 'cash'
|
||||
|
||||
def _is_write_forbidden(self, fields):
|
||||
return bool(fields and self.open_session_ids)
|
||||
|
||||
def write(self, vals):
|
||||
if self._is_write_forbidden(set(vals.keys())):
|
||||
raise UserError(_('Please 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)
|
||||
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {}, config_ids=[(5, 0, 0)])
|
||||
return super().copy(default)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,117 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
from datetime import date
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
available_in_pos = fields.Boolean(string='Available in POS', help='Check if you want this product to appear in the Point of Sale.', default=False)
|
||||
to_weight = fields.Boolean(string='To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration.")
|
||||
pos_categ_id = fields.Many2one(
|
||||
'pos.category', string='Point of Sale Category',
|
||||
help="Category used in the Point of Sale.")
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_open_session(self):
|
||||
product_ctx = dict(self.env.context or {}, active_test=False)
|
||||
if self.with_context(product_ctx).search_count([('id', 'in', self.ids), ('available_in_pos', '=', True)]):
|
||||
if self.env['pos.session'].sudo().search_count([('state', '!=', 'closed')]):
|
||||
raise UserError(_('You cannot delete a product saleable in point of sale while a session is still opened.'))
|
||||
|
||||
@api.onchange('sale_ok')
|
||||
def _onchange_sale_ok(self):
|
||||
if not self.sale_ok:
|
||||
self.available_in_pos = False
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_active_pos_session(self):
|
||||
product_ctx = dict(self.env.context or {}, active_test=False)
|
||||
if self.env['pos.session'].sudo().search_count([('state', '!=', 'closed')]):
|
||||
if self.with_context(product_ctx).search_count([('id', 'in', self.ids), ('product_tmpl_id.available_in_pos', '=', True)]):
|
||||
raise UserError(_('You cannot delete a product saleable in point of sale while a session is still opened.'))
|
||||
|
||||
def get_product_info_pos(self, price, quantity, pos_config_id):
|
||||
self.ensure_one()
|
||||
config = self.env['pos.config'].browse(pos_config_id)
|
||||
|
||||
# Tax related
|
||||
taxes = self.taxes_id.compute_all(price, config.currency_id, quantity, self)
|
||||
grouped_taxes = {}
|
||||
for tax in taxes['taxes']:
|
||||
if tax['id'] in grouped_taxes:
|
||||
grouped_taxes[tax['id']]['amount'] += tax['amount']/quantity if quantity else 0
|
||||
else:
|
||||
grouped_taxes[tax['id']] = {
|
||||
'name': tax['name'],
|
||||
'amount': tax['amount']/quantity if quantity else 0
|
||||
}
|
||||
|
||||
all_prices = {
|
||||
'price_without_tax': taxes['total_excluded']/quantity if quantity else 0,
|
||||
'price_with_tax': taxes['total_included']/quantity if quantity else 0,
|
||||
'tax_details': list(grouped_taxes.values()),
|
||||
}
|
||||
|
||||
# Pricelists
|
||||
if config.use_pricelist:
|
||||
pricelists = config.available_pricelist_ids
|
||||
else:
|
||||
pricelists = config.pricelist_id
|
||||
price_per_pricelist_id = pricelists._price_get(self, quantity)
|
||||
pricelist_list = [{'name': pl.name, 'price': price_per_pricelist_id[pl.id]} for pl in pricelists]
|
||||
|
||||
# Warehouses
|
||||
warehouse_list = [
|
||||
{'name': w.name,
|
||||
'available_quantity': self.with_context({'warehouse': w.id}).qty_available,
|
||||
'forecasted_quantity': self.with_context({'warehouse': w.id}).virtual_available,
|
||||
'uom': self.uom_name}
|
||||
for w in self.env['stock.warehouse'].search([])]
|
||||
|
||||
# Suppliers
|
||||
key = itemgetter('partner_id')
|
||||
supplier_list = []
|
||||
for key, group in groupby(sorted(self.seller_ids, key=key), key=key):
|
||||
for s in list(group):
|
||||
if not((s.date_start and s.date_start > date.today()) or (s.date_end and s.date_end < date.today()) or (s.min_qty > quantity)):
|
||||
supplier_list.append({
|
||||
'name': s.partner_id.name,
|
||||
'delay': s.delay,
|
||||
'price': s.price
|
||||
})
|
||||
break
|
||||
|
||||
# Variants
|
||||
variant_list = [{'name': attribute_line.attribute_id.name,
|
||||
'values': list(map(lambda attr_name: {'name': attr_name, 'search': '%s %s' % (self.name, attr_name)}, attribute_line.value_ids.mapped('name')))}
|
||||
for attribute_line in self.attribute_line_ids]
|
||||
|
||||
return {
|
||||
'all_prices': all_prices,
|
||||
'pricelists': pricelist_list,
|
||||
'warehouses': warehouse_list,
|
||||
'suppliers': supplier_list,
|
||||
'variants': variant_list
|
||||
}
|
||||
|
||||
|
||||
class UomCateg(models.Model):
|
||||
_inherit = 'uom.category'
|
||||
|
||||
is_pos_groupable = fields.Boolean(string='Group Products in POS',
|
||||
help="Check if you want to group products of this category in point of sale orders")
|
||||
|
||||
|
||||
class Uom(models.Model):
|
||||
_inherit = 'uom.uom'
|
||||
|
||||
is_pos_groupable = fields.Boolean(related='category_id.is_pos_groupable', readonly=False)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
point_of_sale_update_stock_quantities = fields.Selection([
|
||||
('closing', 'At the session closing (faster)'),
|
||||
('real', 'In real time (accurate but slower)'),
|
||||
], default='closing', string="Update quantities in stock",
|
||||
help="At the session closing: A picking is created for the entire session when it's closed\n In real time: Each order sent to the server create its own picking")
|
||||
point_of_sale_use_ticket_qr_code = fields.Boolean(
|
||||
string='Use QR code on ticket',
|
||||
help="Add a QR code on the ticket, which the user can scan to request the invoice linked to its order.")
|
||||
|
||||
@api.constrains('period_lock_date', 'fiscalyear_lock_date')
|
||||
def validate_period_lock_date(self):
|
||||
""" This constrains makes it impossible to change the period lock date if
|
||||
some open POS session exists into it. Without that, these POS sessions
|
||||
would trigger an error message saying that the period has been locked when
|
||||
trying to close them.
|
||||
"""
|
||||
pos_session_model = self.env['pos.session'].sudo()
|
||||
for record in self:
|
||||
sessions_in_period = pos_session_model.search(
|
||||
[
|
||||
"&",
|
||||
"&",
|
||||
("company_id", "=", record.id),
|
||||
("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))
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
"""
|
||||
NOTES
|
||||
1. Fields with name starting with 'pos_' are removed from the vals before super call to `create`.
|
||||
Values of these fields are written to `pos_config_id` record after the super call.
|
||||
This is done so that these fields are written at the same time to the active pos.config record.
|
||||
2. During `creation` of this record, each related field is written to the source record
|
||||
*one after the other*, so constraints on the source record that are based on multiple
|
||||
fields might not work properly. However, only the *modified* related fields are written
|
||||
to the source field. But the identification of modified fields happen during the super
|
||||
call, not before `create` is called. Because of this, vals contains a lot of field before
|
||||
super call, then the number of fields is reduced after.
|
||||
"""
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
def _default_pos_config(self):
|
||||
# Default to the last modified pos.config.
|
||||
active_model = self.env.context.get('active_model', '')
|
||||
if active_model == 'pos.config':
|
||||
return self.env.context.get('active_id')
|
||||
return self.env['pos.config'].search([('company_id', '=', self.env.company.id)], order='write_date desc', limit=1)
|
||||
|
||||
pos_config_id = fields.Many2one('pos.config', string="Point of Sale", default=lambda self: self._default_pos_config())
|
||||
sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax", related='company_id.account_sale_tax_id', readonly=False)
|
||||
module_pos_mercury = fields.Boolean(string="Vantiv Payment Terminal", help="The transactions are processed by Vantiv. Set your Vantiv credentials on the related payment method.")
|
||||
module_pos_adyen = fields.Boolean(string="Adyen Payment Terminal", help="The transactions are processed by Adyen. Set your Adyen credentials on the related payment method.")
|
||||
module_pos_stripe = fields.Boolean(string="Stripe Payment Terminal", help="The transactions are processed by Stripe. Set your Stripe credentials on the related payment method.")
|
||||
module_pos_six = fields.Boolean(string="Six Payment Terminal", help="The transactions are processed by Six. Set the IP address of the terminal on the related payment method.")
|
||||
update_stock_quantities = fields.Selection(related="company_id.point_of_sale_update_stock_quantities", readonly=False)
|
||||
account_default_pos_receivable_account_id = fields.Many2one(string='Default Account Receivable (PoS)', related='company_id.account_default_pos_receivable_account_id', readonly=False)
|
||||
is_default_pricelist_displayed = fields.Boolean(compute="_compute_pos_pricelist_id", compute_sudo=True)
|
||||
barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', related='company_id.nomenclature_id', readonly=False)
|
||||
|
||||
# pos.config fields
|
||||
pos_module_pos_discount = fields.Boolean(related='pos_config_id.module_pos_discount', readonly=False)
|
||||
pos_module_pos_hr = fields.Boolean(related='pos_config_id.module_pos_hr', readonly=False)
|
||||
pos_module_pos_restaurant = fields.Boolean(related='pos_config_id.module_pos_restaurant', readonly=False)
|
||||
|
||||
pos_allowed_pricelist_ids = fields.Many2many('product.pricelist', compute='_compute_pos_allowed_pricelist_ids')
|
||||
pos_amount_authorized_diff = fields.Float(related='pos_config_id.amount_authorized_diff', readonly=False)
|
||||
pos_available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists', compute='_compute_pos_pricelist_id', readonly=False, store=True)
|
||||
pos_cash_control = fields.Boolean(related='pos_config_id.cash_control')
|
||||
pos_cash_rounding = fields.Boolean(related='pos_config_id.cash_rounding', readonly=False, string="Cash Rounding (PoS)")
|
||||
pos_company_has_template = fields.Boolean(related='pos_config_id.company_has_template')
|
||||
pos_default_bill_ids = fields.Many2many(related='pos_config_id.default_bill_ids', readonly=False)
|
||||
pos_default_fiscal_position_id = fields.Many2one('account.fiscal.position', string='Default Fiscal Position', compute='_compute_pos_fiscal_positions', readonly=False, store=True)
|
||||
pos_fiscal_position_ids = fields.Many2many('account.fiscal.position', string='Fiscal Positions', compute='_compute_pos_fiscal_positions', readonly=False, store=True)
|
||||
pos_has_active_session = fields.Boolean(related='pos_config_id.has_active_session')
|
||||
pos_iface_available_categ_ids = fields.Many2many('pos.category', string='Available PoS Product Categories', compute='_compute_pos_iface_available_categ_ids', readonly=False, store=True)
|
||||
pos_iface_big_scrollbars = fields.Boolean(related='pos_config_id.iface_big_scrollbars', readonly=False)
|
||||
pos_iface_cashdrawer = fields.Boolean(string='Cashdrawer', compute='_compute_pos_iface_cashdrawer', readonly=False, store=True)
|
||||
pos_iface_customer_facing_display_local = fields.Boolean(related='pos_config_id.iface_customer_facing_display_local', readonly=False)
|
||||
pos_iface_customer_facing_display_via_proxy = fields.Boolean(string='Customer Facing Display', compute='_compute_pos_iface_customer_facing_display_via_proxy', readonly=False, store=True)
|
||||
pos_iface_electronic_scale = fields.Boolean(string='Electronic Scale', compute='_compute_pos_iface_electronic_scale', readonly=False, store=True)
|
||||
pos_iface_print_auto = fields.Boolean(related='pos_config_id.iface_print_auto', readonly=False)
|
||||
pos_iface_print_skip_screen = fields.Boolean(related='pos_config_id.iface_print_skip_screen', readonly=False)
|
||||
pos_iface_print_via_proxy = fields.Boolean(string='Print via Proxy', compute='_compute_pos_iface_print_via_proxy', readonly=False, store=True)
|
||||
pos_iface_scan_via_proxy = fields.Boolean(string='Scan via Proxy', compute='_compute_pos_iface_scan_via_proxy', readonly=False, store=True)
|
||||
pos_iface_start_categ_id = fields.Many2one('pos.category', string='Initial Category', compute='_compute_pos_iface_start_categ_id', readonly=False, store=True)
|
||||
pos_iface_tax_included = fields.Selection(related='pos_config_id.iface_tax_included', readonly=False)
|
||||
pos_iface_tipproduct = fields.Boolean(related='pos_config_id.iface_tipproduct', readonly=False)
|
||||
pos_invoice_journal_id = fields.Many2one(related='pos_config_id.invoice_journal_id', readonly=False)
|
||||
pos_is_header_or_footer = fields.Boolean(related='pos_config_id.is_header_or_footer', readonly=False)
|
||||
pos_is_margins_costs_accessible_to_every_user = fields.Boolean(related='pos_config_id.is_margins_costs_accessible_to_every_user', readonly=False)
|
||||
pos_is_posbox = fields.Boolean(related='pos_config_id.is_posbox', readonly=False)
|
||||
pos_journal_id = fields.Many2one(related='pos_config_id.journal_id', readonly=False)
|
||||
pos_limit_categories = fields.Boolean(related='pos_config_id.limit_categories', readonly=False)
|
||||
pos_limited_partners_amount = fields.Integer(related='pos_config_id.limited_partners_amount', readonly=False)
|
||||
pos_limited_partners_loading = fields.Boolean(related='pos_config_id.limited_partners_loading', readonly=False)
|
||||
pos_limited_products_amount = fields.Integer(related='pos_config_id.limited_products_amount', readonly=False)
|
||||
pos_limited_products_loading = fields.Boolean(related='pos_config_id.limited_products_loading', readonly=False)
|
||||
pos_manual_discount = fields.Boolean(related='pos_config_id.manual_discount', readonly=False)
|
||||
pos_only_round_cash_method = fields.Boolean(related='pos_config_id.only_round_cash_method', readonly=False)
|
||||
pos_other_devices = fields.Boolean(related='pos_config_id.other_devices', readonly=False)
|
||||
pos_partner_load_background = fields.Boolean(related='pos_config_id.partner_load_background', readonly=False)
|
||||
pos_payment_method_ids = fields.Many2many(related='pos_config_id.payment_method_ids', readonly=False)
|
||||
pos_picking_policy = fields.Selection(related='pos_config_id.picking_policy', readonly=False)
|
||||
pos_picking_type_id = fields.Many2one(related='pos_config_id.picking_type_id', readonly=False)
|
||||
pos_pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist', compute='_compute_pos_pricelist_id', readonly=False, store=True)
|
||||
pos_product_load_background = fields.Boolean(related='pos_config_id.product_load_background', readonly=False)
|
||||
pos_proxy_ip = fields.Char(string='IP Address', compute='_compute_pos_proxy_ip', readonly=False, store=True)
|
||||
pos_receipt_footer = fields.Text(string='Receipt Footer', compute='_compute_pos_receipt_header_footer', readonly=False, store=True)
|
||||
pos_receipt_header = fields.Text(string='Receipt Header', compute='_compute_pos_receipt_header_footer', readonly=False, store=True)
|
||||
pos_restrict_price_control = fields.Boolean(related='pos_config_id.restrict_price_control', readonly=False)
|
||||
pos_rounding_method = fields.Many2one(related='pos_config_id.rounding_method', readonly=False)
|
||||
pos_route_id = fields.Many2one(related='pos_config_id.route_id', readonly=False)
|
||||
pos_selectable_categ_ids = fields.Many2many('pos.category', compute='_compute_pos_selectable_categ_ids')
|
||||
pos_sequence_id = fields.Many2one(related='pos_config_id.sequence_id')
|
||||
pos_set_maximum_difference = fields.Boolean(related='pos_config_id.set_maximum_difference', readonly=False)
|
||||
pos_ship_later = fields.Boolean(related='pos_config_id.ship_later', readonly=False)
|
||||
pos_start_category = fields.Boolean(related='pos_config_id.start_category', readonly=False)
|
||||
pos_tax_regime_selection = fields.Boolean(related='pos_config_id.tax_regime_selection', readonly=False)
|
||||
pos_tip_product_id = fields.Many2one('product.product', string='Tip Product', compute='_compute_pos_tip_product_id', readonly=False, store=True)
|
||||
pos_use_pricelist = fields.Boolean(related='pos_config_id.use_pricelist', readonly=False)
|
||||
pos_warehouse_id = fields.Many2one(related='pos_config_id.warehouse_id', readonly=False, string="Warehouse (PoS)")
|
||||
point_of_sale_use_ticket_qr_code = fields.Boolean(related='company_id.point_of_sale_use_ticket_qr_code', readonly=False)
|
||||
|
||||
@api.model
|
||||
def _keep_new_vals(self, pos_config, pos_fields_vals):
|
||||
""" Keep vals in pos_fields_vals that are different than
|
||||
pos_config's values.
|
||||
"""
|
||||
new_vals = {}
|
||||
for field, val in pos_fields_vals.items():
|
||||
if pos_config._fields.get(field):
|
||||
cache_value = pos_config._fields.get(field).convert_to_cache(val, pos_config)
|
||||
record_value = pos_config._fields.get(field).convert_to_record(cache_value, pos_config)
|
||||
if record_value != pos_config[field]:
|
||||
new_vals[field] = val
|
||||
return new_vals
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# STEP: Remove the 'pos' fields from each vals.
|
||||
# They will be written atomically to `pos_config_id` after the super call.
|
||||
pos_config_id_to_fields_vals_map = {}
|
||||
|
||||
for vals in vals_list:
|
||||
pos_config_id = vals.get('pos_config_id')
|
||||
if pos_config_id:
|
||||
pos_fields_vals = {}
|
||||
|
||||
if vals.get('pos_cash_rounding'):
|
||||
vals['group_cash_rounding'] = True
|
||||
|
||||
if vals.get('pos_use_pricelist'):
|
||||
vals['group_product_pricelist'] = True
|
||||
|
||||
for field in self._fields.values():
|
||||
if field.name == 'pos_config_id':
|
||||
continue
|
||||
|
||||
val = vals.get(field.name)
|
||||
|
||||
# Add only to pos_fields_vals if
|
||||
# 1. _field is in vals -- meaning, the _field is in view.
|
||||
# 2. _field starts with 'pos_' -- meaning, the _field is a pos field.
|
||||
if field.name.startswith('pos_') and val is not None:
|
||||
pos_config_field_name = field.name[4:]
|
||||
if not pos_config_field_name in self.env['pos.config']._fields:
|
||||
_logger.warning("The value of '%s' is not properly saved to the pos_config_id field because the destination"
|
||||
" field '%s' is not a valid field in the pos.config model.", field.name, pos_config_field_name)
|
||||
else:
|
||||
pos_fields_vals[pos_config_field_name] = val
|
||||
del vals[field.name]
|
||||
|
||||
pos_config_id_to_fields_vals_map[pos_config_id] = pos_fields_vals
|
||||
|
||||
# STEP: Call super on the modified vals_list.
|
||||
# NOTE: When creating `res.config.settings` records, it doesn't write on *unmodified* related fields.
|
||||
result = super().create(vals_list)
|
||||
|
||||
# STEP: Finally, we write the value of 'pos' fields to 'pos_config_id'.
|
||||
for pos_config_id, pos_fields_vals in pos_config_id_to_fields_vals_map.items():
|
||||
pos_config = self.env['pos.config'].browse(pos_config_id)
|
||||
pos_fields_vals = self._keep_new_vals(pos_config, pos_fields_vals)
|
||||
pos_config.write(pos_fields_vals)
|
||||
|
||||
return result
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
if not self.group_product_pricelist:
|
||||
self.env['pos.config'].search([
|
||||
('use_pricelist', '=', True)
|
||||
]).use_pricelist = False
|
||||
|
||||
if not self.group_cash_rounding:
|
||||
self.env['pos.config'].search([
|
||||
('cash_rounding', '=', True)
|
||||
]).cash_rounding = False
|
||||
|
||||
def action_pos_config_create_new(self):
|
||||
return {
|
||||
'view_mode': 'form',
|
||||
'res_model': 'pos.config',
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'res_id': False,
|
||||
'context': {'pos_config_open_modal': True, 'pos_config_create_mode': True},
|
||||
}
|
||||
|
||||
def pos_open_ui(self):
|
||||
if self._context.get('pos_config_id'):
|
||||
pos_config_id = self._context['pos_config_id']
|
||||
pos_config = self.env['pos.config'].browse(pos_config_id)
|
||||
return pos_config.open_ui()
|
||||
|
||||
@api.model
|
||||
def _is_cashdrawer_displayed(self, res_config):
|
||||
return res_config.pos_iface_print_via_proxy
|
||||
|
||||
@api.depends('pos_limit_categories', 'pos_config_id')
|
||||
def _compute_pos_iface_available_categ_ids(self):
|
||||
for res_config in self:
|
||||
if not res_config.pos_limit_categories:
|
||||
res_config.pos_iface_available_categ_ids = False
|
||||
else:
|
||||
res_config.pos_iface_available_categ_ids = res_config.pos_config_id.iface_available_categ_ids
|
||||
|
||||
@api.depends('pos_start_category', 'pos_config_id')
|
||||
def _compute_pos_iface_start_categ_id(self):
|
||||
for res_config in self:
|
||||
if not res_config.pos_start_category:
|
||||
res_config.pos_iface_start_categ_id = False
|
||||
else:
|
||||
res_config.pos_iface_start_categ_id = res_config.pos_config_id.iface_start_categ_id
|
||||
|
||||
@api.depends('pos_iface_available_categ_ids')
|
||||
def _compute_pos_selectable_categ_ids(self):
|
||||
for res_config in self:
|
||||
if res_config.pos_iface_available_categ_ids:
|
||||
res_config.pos_selectable_categ_ids = res_config.pos_iface_available_categ_ids
|
||||
else:
|
||||
res_config.pos_selectable_categ_ids = self.env['pos.category'].search([])
|
||||
|
||||
@api.depends('pos_iface_print_via_proxy', 'pos_config_id')
|
||||
def _compute_pos_iface_cashdrawer(self):
|
||||
for res_config in self:
|
||||
if self._is_cashdrawer_displayed(res_config):
|
||||
res_config.pos_iface_cashdrawer = res_config.pos_config_id.iface_cashdrawer
|
||||
else:
|
||||
res_config.pos_iface_cashdrawer = False
|
||||
|
||||
@api.depends('pos_is_header_or_footer', 'pos_config_id')
|
||||
def _compute_pos_receipt_header_footer(self):
|
||||
for res_config in self:
|
||||
if res_config.pos_is_header_or_footer:
|
||||
res_config.pos_receipt_header = res_config.pos_config_id.receipt_header
|
||||
res_config.pos_receipt_footer = res_config.pos_config_id.receipt_footer
|
||||
else:
|
||||
res_config.pos_receipt_header = False
|
||||
res_config.pos_receipt_footer = False
|
||||
|
||||
@api.depends('pos_tax_regime_selection', 'pos_config_id')
|
||||
def _compute_pos_fiscal_positions(self):
|
||||
for res_config in self:
|
||||
if res_config.pos_tax_regime_selection:
|
||||
res_config.pos_default_fiscal_position_id = res_config.pos_config_id.default_fiscal_position_id
|
||||
res_config.pos_fiscal_position_ids = res_config.pos_config_id.fiscal_position_ids
|
||||
else:
|
||||
res_config.pos_default_fiscal_position_id = False
|
||||
res_config.pos_fiscal_position_ids = [(5, 0, 0)]
|
||||
|
||||
@api.depends('pos_iface_tipproduct', 'pos_config_id')
|
||||
def _compute_pos_tip_product_id(self):
|
||||
for res_config in self:
|
||||
if res_config.pos_iface_tipproduct:
|
||||
res_config.pos_tip_product_id = res_config.pos_config_id.tip_product_id
|
||||
else:
|
||||
res_config.pos_tip_product_id = False
|
||||
|
||||
@api.depends('pos_use_pricelist', 'pos_config_id', 'pos_journal_id')
|
||||
def _compute_pos_pricelist_id(self):
|
||||
for res_config in self:
|
||||
currency_id = res_config.pos_journal_id.currency_id.id if res_config.pos_journal_id.currency_id else res_config.pos_config_id.company_id.currency_id.id
|
||||
pricelists_in_current_currency = self.env['product.pricelist'].search([('company_id', 'in', (False, res_config.pos_config_id.company_id.id)), ('currency_id', '=', currency_id)])
|
||||
if not res_config.pos_use_pricelist:
|
||||
res_config.pos_available_pricelist_ids = pricelists_in_current_currency[:1]
|
||||
res_config.pos_pricelist_id = pricelists_in_current_currency[:1]
|
||||
else:
|
||||
if any([p.currency_id.id != currency_id for p in res_config.pos_available_pricelist_ids]):
|
||||
res_config.pos_available_pricelist_ids = pricelists_in_current_currency
|
||||
res_config.pos_pricelist_id = pricelists_in_current_currency[:1]
|
||||
else:
|
||||
res_config.pos_available_pricelist_ids = res_config.pos_config_id.available_pricelist_ids
|
||||
res_config.pos_pricelist_id = res_config.pos_config_id.pricelist_id
|
||||
|
||||
# TODO: Remove this field in master because it's always True.
|
||||
res_config.is_default_pricelist_displayed = True
|
||||
|
||||
@api.depends('pos_available_pricelist_ids', 'pos_use_pricelist')
|
||||
def _compute_pos_allowed_pricelist_ids(self):
|
||||
for res_config in self:
|
||||
if res_config.pos_use_pricelist:
|
||||
res_config.pos_allowed_pricelist_ids = res_config.pos_available_pricelist_ids.ids
|
||||
else:
|
||||
res_config.pos_allowed_pricelist_ids = self.env['product.pricelist'].search([]).ids
|
||||
|
||||
@api.depends('pos_is_posbox', 'pos_config_id')
|
||||
def _compute_pos_proxy_ip(self):
|
||||
for res_config in self:
|
||||
if not res_config.pos_is_posbox:
|
||||
res_config.pos_proxy_ip = False
|
||||
else:
|
||||
res_config.pos_proxy_ip = res_config.pos_config_id.proxy_ip
|
||||
|
||||
@api.depends('pos_is_posbox', 'pos_config_id')
|
||||
def _compute_pos_iface_print_via_proxy(self):
|
||||
for res_config in self:
|
||||
if not res_config.pos_is_posbox:
|
||||
res_config.pos_iface_print_via_proxy = False
|
||||
else:
|
||||
res_config.pos_iface_print_via_proxy = res_config.pos_config_id.iface_print_via_proxy
|
||||
|
||||
@api.depends('pos_is_posbox', 'pos_config_id')
|
||||
def _compute_pos_iface_scan_via_proxy(self):
|
||||
for res_config in self:
|
||||
if not res_config.pos_is_posbox:
|
||||
res_config.pos_iface_scan_via_proxy = False
|
||||
else:
|
||||
res_config.pos_iface_scan_via_proxy = res_config.pos_config_id.iface_scan_via_proxy
|
||||
|
||||
@api.depends('pos_is_posbox', 'pos_config_id')
|
||||
def _compute_pos_iface_electronic_scale(self):
|
||||
for res_config in self:
|
||||
if not res_config.pos_is_posbox:
|
||||
res_config.pos_iface_electronic_scale = False
|
||||
else:
|
||||
res_config.pos_iface_electronic_scale = res_config.pos_config_id.iface_electronic_scale
|
||||
|
||||
@api.depends('pos_is_posbox', 'pos_config_id')
|
||||
def _compute_pos_iface_customer_facing_display_via_proxy(self):
|
||||
for res_config in self:
|
||||
if not res_config.pos_is_posbox:
|
||||
res_config.pos_iface_customer_facing_display_via_proxy = False
|
||||
else:
|
||||
res_config.pos_iface_customer_facing_display_via_proxy = res_config.pos_config_id.iface_customer_facing_display_via_proxy
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
pos_order_count = fields.Integer(
|
||||
compute='_compute_pos_order',
|
||||
help="The number of point of sales orders related to this customer",
|
||||
groups="point_of_sale.group_pos_user",
|
||||
)
|
||||
pos_order_ids = fields.One2many('pos.order', 'partner_id', readonly=True)
|
||||
|
||||
def _compute_pos_order(self):
|
||||
# retrieve all children partners and prefetch 'parent_id' on them
|
||||
all_partners = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
|
||||
all_partners.read(['parent_id'])
|
||||
|
||||
pos_order_data = self.env['pos.order']._read_group(
|
||||
domain=[('partner_id', 'in', all_partners.ids)],
|
||||
fields=['partner_id'], groupby=['partner_id']
|
||||
)
|
||||
|
||||
self.pos_order_count = 0
|
||||
for group in pos_order_data:
|
||||
partner = self.browse(group['partner_id'][0])
|
||||
while partner:
|
||||
if partner in self:
|
||||
partner.pos_order_count += group['partner_id_count']
|
||||
partner = partner.parent_id
|
||||
|
||||
def action_view_pos_order(self):
|
||||
'''
|
||||
This function returns an action that displays the pos orders from partner.
|
||||
'''
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('point_of_sale.action_pos_pos_form')
|
||||
if self.is_company:
|
||||
action['domain'] = [('partner_id.commercial_partner_id', '=', self.id)]
|
||||
else:
|
||||
action['domain'] = [('partner_id', '=', self.id)]
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def create_from_ui(self, partner):
|
||||
""" create or modify a partner from the point of sale ui.
|
||||
partner contains the partner's fields. """
|
||||
# image is a dataurl, get the data after the comma
|
||||
if partner.get('image_1920'):
|
||||
partner['image_1920'] = partner['image_1920'].split(',')[1]
|
||||
partner_id = partner.pop('id', False)
|
||||
if partner_id: # Modifying existing partner
|
||||
self.browse(partner_id).write(partner)
|
||||
else:
|
||||
partner_id = self.create(partner).id
|
||||
return partner_id
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_active_pos_session(self):
|
||||
running_sessions = self.env['pos.session'].sudo().search([('state', '!=', 'closed')])
|
||||
if running_sessions:
|
||||
raise UserError(
|
||||
_("You cannot delete contacts while there are active PoS sessions. Close the session(s) %s first.")
|
||||
% ", ".join(session.name for session in running_sessions)
|
||||
)
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_is_zero, float_compare
|
||||
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit='stock.picking'
|
||||
|
||||
pos_session_id = fields.Many2one('pos.session', index=True)
|
||||
pos_order_id = fields.Many2one('pos.order', index=True)
|
||||
|
||||
def _prepare_picking_vals(self, partner, picking_type, location_id, location_dest_id):
|
||||
return {
|
||||
'partner_id': partner.id if partner else False,
|
||||
'user_id': False,
|
||||
'picking_type_id': picking_type.id,
|
||||
'move_type': 'direct',
|
||||
'location_id': location_id,
|
||||
'location_dest_id': location_dest_id,
|
||||
}
|
||||
|
||||
|
||||
@api.model
|
||||
def _create_picking_from_pos_order_lines(self, location_dest_id, lines, picking_type, partner=False):
|
||||
"""We'll create some picking based on order_lines"""
|
||||
|
||||
pickings = self.env['stock.picking']
|
||||
stockable_lines = lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not float_is_zero(l.qty, precision_rounding=l.product_id.uom_id.rounding))
|
||||
if not stockable_lines:
|
||||
return pickings
|
||||
positive_lines = stockable_lines.filtered(lambda l: l.qty > 0)
|
||||
negative_lines = stockable_lines - positive_lines
|
||||
|
||||
if positive_lines:
|
||||
location_id = picking_type.default_location_src_id.id
|
||||
positive_picking = self.env['stock.picking'].create(
|
||||
self._prepare_picking_vals(partner, picking_type, location_id, location_dest_id)
|
||||
)
|
||||
|
||||
positive_picking._create_move_from_pos_order_lines(positive_lines)
|
||||
self.env.flush_all()
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
positive_picking._action_done()
|
||||
except (UserError, ValidationError):
|
||||
pass
|
||||
|
||||
pickings |= positive_picking
|
||||
if negative_lines:
|
||||
if picking_type.return_picking_type_id:
|
||||
return_picking_type = picking_type.return_picking_type_id
|
||||
return_location_id = return_picking_type.default_location_dest_id.id
|
||||
else:
|
||||
return_picking_type = picking_type
|
||||
return_location_id = picking_type.default_location_src_id.id
|
||||
|
||||
negative_picking = self.env['stock.picking'].create(
|
||||
self._prepare_picking_vals(partner, return_picking_type, location_dest_id, return_location_id)
|
||||
)
|
||||
negative_picking._create_move_from_pos_order_lines(negative_lines)
|
||||
self.env.flush_all()
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
negative_picking._action_done()
|
||||
except (UserError, ValidationError):
|
||||
pass
|
||||
pickings |= negative_picking
|
||||
return pickings
|
||||
|
||||
def _prepare_stock_move_vals(self, first_line, order_lines):
|
||||
return {
|
||||
'name': first_line.name,
|
||||
'product_uom': first_line.product_id.uom_id.id,
|
||||
'picking_id': self.id,
|
||||
'picking_type_id': self.picking_type_id.id,
|
||||
'product_id': first_line.product_id.id,
|
||||
'product_uom_qty': abs(sum(order_lines.mapped('qty'))),
|
||||
'state': 'draft',
|
||||
'location_id': self.location_id.id,
|
||||
'location_dest_id': self.location_dest_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
}
|
||||
|
||||
def _create_move_from_pos_order_lines(self, lines):
|
||||
self.ensure_one()
|
||||
lines_by_product = groupby(sorted(lines, key=lambda l: l.product_id.id), key=lambda l: l.product_id.id)
|
||||
move_vals = []
|
||||
for dummy, olines in lines_by_product:
|
||||
order_lines = self.env['pos.order.line'].concat(*olines)
|
||||
move_vals.append(self._prepare_stock_move_vals(order_lines[0], order_lines))
|
||||
moves = self.env['stock.move'].create(move_vals)
|
||||
confirmed_moves = moves._action_confirm()
|
||||
confirmed_moves._add_mls_related_to_order(lines, are_qties_done=True)
|
||||
self._link_owner_on_return_picking(lines)
|
||||
|
||||
def _link_owner_on_return_picking(self, lines):
|
||||
"""This method tries to retrieve the owner of the returned product"""
|
||||
if lines[0].order_id.refunded_order_ids.picking_ids:
|
||||
returned_lines_picking = lines[0].order_id.refunded_order_ids.picking_ids
|
||||
returnable_qty_by_product = {}
|
||||
for move_line in returned_lines_picking.move_line_ids:
|
||||
returnable_qty_by_product[(move_line.product_id.id, move_line.owner_id.id or 0)] = move_line.qty_done
|
||||
for move in self.move_line_ids:
|
||||
for keys in returnable_qty_by_product:
|
||||
if move.product_id.id == keys[0] and keys[1] and returnable_qty_by_product[keys] > 0:
|
||||
move.write({'owner_id': keys[1]})
|
||||
returnable_qty_by_product[keys] -= move.qty_done
|
||||
|
||||
|
||||
def _send_confirmation_email(self):
|
||||
# Avoid sending Mail/SMS for POS deliveries
|
||||
pickings = self.filtered(lambda p: p.picking_type_id != p.picking_type_id.warehouse_id.pos_type_id)
|
||||
return super(StockPicking, pickings)._send_confirmation_email()
|
||||
|
||||
def _action_done(self):
|
||||
res = super()._action_done()
|
||||
for rec in self:
|
||||
if rec.picking_type_id.code != 'outgoing':
|
||||
continue
|
||||
if rec.pos_order_id.to_ship and not rec.pos_order_id.to_invoice:
|
||||
cost_per_account = defaultdict(lambda: 0.0)
|
||||
for line in rec.pos_order_id.lines:
|
||||
if line.product_id.type != 'product' or line.product_id.valuation != 'real_time':
|
||||
continue
|
||||
out = line.product_id.categ_id.property_stock_account_output_categ_id
|
||||
exp = line.product_id._get_product_accounts()['expense']
|
||||
cost_per_account[(out, exp)] += line.total_cost
|
||||
move_vals = []
|
||||
for (out_acc, exp_acc), cost in cost_per_account.items():
|
||||
move_vals.append({
|
||||
'journal_id': rec.pos_order_id.sale_journal.id,
|
||||
'date': rec.pos_order_id.date_order,
|
||||
'ref': 'pos_order_'+str(rec.pos_order_id.id),
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'name': rec.pos_order_id.name,
|
||||
'account_id': exp_acc.id,
|
||||
'debit': cost,
|
||||
'credit': 0.0,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': rec.pos_order_id.name,
|
||||
'account_id': out_acc.id,
|
||||
'debit': 0.0,
|
||||
'credit': cost,
|
||||
}),
|
||||
],
|
||||
})
|
||||
move = self.env['account.move'].sudo().create(move_vals)
|
||||
move.action_post()
|
||||
return res
|
||||
|
||||
class StockPickingType(models.Model):
|
||||
_inherit = 'stock.picking.type'
|
||||
|
||||
@api.depends('warehouse_id')
|
||||
def _compute_hide_reservation_method(self):
|
||||
super()._compute_hide_reservation_method()
|
||||
for picking_type in self:
|
||||
if picking_type == picking_type.warehouse_id.pos_type_id:
|
||||
picking_type.hide_reservation_method = True
|
||||
|
||||
@api.constrains('active')
|
||||
def _check_active(self):
|
||||
for picking_type in self:
|
||||
if picking_type.active:
|
||||
continue
|
||||
pos_config = self.env['pos.config'].sudo().search([('picking_type_id', '=', picking_type.id)], limit=1)
|
||||
if pos_config:
|
||||
raise ValidationError(_("You cannot archive '%s' as it is used by a POS configuration '%s'.", picking_type.name, pos_config.name))
|
||||
|
||||
class ProcurementGroup(models.Model):
|
||||
_inherit = 'procurement.group'
|
||||
|
||||
pos_order_id = fields.Many2one('pos.order', 'POS Order')
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
def _get_new_picking_values(self):
|
||||
vals = super(StockMove, self)._get_new_picking_values()
|
||||
vals['pos_session_id'] = self.mapped('group_id.pos_order_id.session_id').id
|
||||
vals['pos_order_id'] = self.mapped('group_id.pos_order_id').id
|
||||
return vals
|
||||
|
||||
def _key_assign_picking(self):
|
||||
keys = super(StockMove, self)._key_assign_picking()
|
||||
return keys + (self.group_id.pos_order_id,)
|
||||
|
||||
@api.model
|
||||
def _prepare_lines_data_dict(self, order_lines):
|
||||
lines_data = defaultdict(dict)
|
||||
for product_id, olines in groupby(sorted(order_lines, key=lambda l: l.product_id.id), key=lambda l: l.product_id.id):
|
||||
lines_data[product_id].update({'order_lines': self.env['pos.order.line'].concat(*olines)})
|
||||
return lines_data
|
||||
|
||||
def _complete_done_qties(self, set_quantity_done_on_move=False):
|
||||
self._action_assign()
|
||||
for move_line in self.move_line_ids:
|
||||
move_line.qty_done = move_line.reserved_uom_qty
|
||||
mls_vals = []
|
||||
moves_to_set = set()
|
||||
for move in self:
|
||||
if float_compare(move.product_uom_qty, move.quantity_done, precision_rounding=move.product_uom.rounding) > 0:
|
||||
remaining_qty = move.product_uom_qty - move.quantity_done
|
||||
mls_vals.append(dict(move._prepare_move_line_vals(), qty_done=remaining_qty))
|
||||
moves_to_set.add(move.id)
|
||||
self.env['stock.move.line'].create(mls_vals)
|
||||
if set_quantity_done_on_move:
|
||||
for move in self.env['stock.move'].browse(moves_to_set):
|
||||
move.quantity_done = move.product_uom_qty
|
||||
|
||||
def _create_production_lots_for_pos_order(self, lines):
|
||||
''' Search for existing lots and create missing ones.
|
||||
|
||||
:param lines: pos order lines with pack lot ids.
|
||||
:type lines: pos.order.line recordset.
|
||||
|
||||
:return stock.lot recordset.
|
||||
'''
|
||||
valid_lots = self.env['stock.lot']
|
||||
moves = self.filtered(lambda m: m.picking_type_id.use_existing_lots)
|
||||
# Already called in self._action_confirm() but just to be safe when coming from _launch_stock_rule_from_pos_order_lines.
|
||||
self._check_company()
|
||||
if moves:
|
||||
moves_product_ids = set(moves.mapped('product_id').ids)
|
||||
lots = lines.pack_lot_ids.filtered(lambda l: l.lot_name and l.product_id.id in moves_product_ids)
|
||||
lots_data = set(lots.mapped(lambda l: (l.product_id.id, l.lot_name)))
|
||||
existing_lots = self.env['stock.lot'].search([
|
||||
('company_id', '=', moves[0].picking_type_id.company_id.id),
|
||||
('product_id', 'in', lines.product_id.ids),
|
||||
('name', 'in', lots.mapped('lot_name')),
|
||||
])
|
||||
#The previous search may return (product_id.id, lot_name) combinations that have no matching in lines.pack_lot_ids.
|
||||
for lot in existing_lots:
|
||||
if (lot.product_id.id, lot.name) in lots_data:
|
||||
valid_lots |= lot
|
||||
lots_data.remove((lot.product_id.id, lot.name))
|
||||
moves = moves.filtered(lambda m: m.picking_type_id.use_create_lots)
|
||||
if moves:
|
||||
moves_product_ids = set(moves.mapped('product_id').ids)
|
||||
missing_lot_values = []
|
||||
for lot_product_id, lot_name in filter(lambda l: l[0] in moves_product_ids, lots_data):
|
||||
missing_lot_values.append({'company_id': self.company_id.id, 'product_id': lot_product_id, 'name': lot_name})
|
||||
valid_lots |= self.env['stock.lot'].create(missing_lot_values)
|
||||
return valid_lots
|
||||
|
||||
def _add_mls_related_to_order(self, related_order_lines, are_qties_done=True):
|
||||
lines_data = self._prepare_lines_data_dict(related_order_lines)
|
||||
qty_fname = 'qty_done' if are_qties_done else 'reserved_uom_qty'
|
||||
# Moves with product_id not in related_order_lines. This can happend e.g. when product_id has a phantom-type bom.
|
||||
moves_to_assign = self.filtered(lambda m: m.product_id.id not in lines_data or m.product_id.tracking == 'none'
|
||||
or (not m.picking_type_id.use_existing_lots and not m.picking_type_id.use_create_lots))
|
||||
moves_to_assign._complete_done_qties(set_quantity_done_on_move=True)
|
||||
moves_remaining = self - moves_to_assign
|
||||
existing_lots = moves_remaining._create_production_lots_for_pos_order(related_order_lines)
|
||||
move_lines_to_create = []
|
||||
mls_qties = []
|
||||
if are_qties_done:
|
||||
for move in moves_remaining:
|
||||
for line in lines_data[move.product_id.id]['order_lines']:
|
||||
sum_of_lots = 0
|
||||
for lot in line.pack_lot_ids.filtered(lambda l: l.lot_name):
|
||||
if line.product_id.tracking == 'serial':
|
||||
qty = 1
|
||||
else:
|
||||
qty = abs(line.qty)
|
||||
ml_vals = dict(move._prepare_move_line_vals())
|
||||
if existing_lots:
|
||||
existing_lot = existing_lots.filtered_domain([('product_id', '=', line.product_id.id), ('name', '=', lot.lot_name)])
|
||||
quant = self.env['stock.quant']
|
||||
if existing_lot:
|
||||
quant = self.env['stock.quant'].search(
|
||||
[('lot_id', '=', existing_lot.id), ('quantity', '>', '0.0'), ('location_id', 'child_of', move.location_id.id)],
|
||||
order='id desc',
|
||||
limit=1
|
||||
)
|
||||
ml_vals.update({
|
||||
'lot_id': existing_lot.id,
|
||||
'location_id': quant.location_id.id or move.location_id.id,
|
||||
'owner_id': quant.owner_id.id or False,
|
||||
})
|
||||
else:
|
||||
ml_vals.update({'lot_name': lot.lot_name})
|
||||
move_lines_to_create.append(ml_vals)
|
||||
mls_qties.append(qty)
|
||||
sum_of_lots += qty
|
||||
if abs(line.qty) != sum_of_lots:
|
||||
difference_qty = abs(line.qty) - sum_of_lots
|
||||
ml_vals = move._prepare_move_line_vals()
|
||||
if line.product_id.tracking == 'serial':
|
||||
move_lines_to_create.extend([ml_vals for i in range(int(difference_qty))])
|
||||
mls_qties.extend([1]*int(difference_qty))
|
||||
else:
|
||||
move_lines_to_create.append(ml_vals)
|
||||
mls_qties.append(difference_qty)
|
||||
move_lines = self.env['stock.move.line'].create(move_lines_to_create)
|
||||
for move_line, qty in zip(move_lines, mls_qties):
|
||||
move_line.write({qty_fname: qty})
|
||||
else:
|
||||
for move in moves_remaining:
|
||||
for line in lines_data[move.product_id.id]['order_lines']:
|
||||
for lot in line.pack_lot_ids.filtered(lambda l: l.lot_name):
|
||||
if line.product_id.tracking == 'serial':
|
||||
qty = 1
|
||||
else:
|
||||
qty = abs(line.qty)
|
||||
if existing_lots:
|
||||
existing_lot = existing_lots.filtered_domain([('product_id', '=', line.product_id.id), ('name', '=', lot.lot_name)])
|
||||
if existing_lot:
|
||||
available_quantity = move._get_available_quantity(move.location_id, lot_id=existing_lot, strict=True)
|
||||
if not float_is_zero(available_quantity, precision_rounding=line.product_id.uom_id.rounding):
|
||||
move._update_reserved_quantity(qty, min(qty, available_quantity), move.location_id, existing_lot)
|
||||
continue
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockRule(models.Model):
|
||||
_inherit = 'stock.rule'
|
||||
|
||||
def _get_stock_move_values(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values):
|
||||
move_values = super()._get_stock_move_values(product_id, product_qty, product_uom, location_id, name, origin, company_id, values)
|
||||
if values.get('product_description_variants') and values.get('group_id') and values['group_id'].pos_order_id:
|
||||
move_values['description_picking'] = values['product_description_variants']
|
||||
return move_values
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class Warehouse(models.Model):
|
||||
_inherit = "stock.warehouse"
|
||||
|
||||
pos_type_id = fields.Many2one('stock.picking.type', string="Point of Sale Operation Type")
|
||||
|
||||
def _get_sequence_values(self, name=False, code=False):
|
||||
sequence_values = super(Warehouse, self)._get_sequence_values(name=name, code=code)
|
||||
sequence_values.update({
|
||||
'pos_type_id': {
|
||||
'name': self.name + ' ' + _('Picking POS'),
|
||||
'prefix': self.code + '/' + (self.pos_type_id.sequence_code or 'POS') + '/',
|
||||
'padding': 5,
|
||||
'company_id': self.company_id.id,
|
||||
}
|
||||
})
|
||||
return sequence_values
|
||||
|
||||
def _get_picking_type_update_values(self):
|
||||
picking_type_update_values = super(Warehouse, self)._get_picking_type_update_values()
|
||||
picking_type_update_values.update({
|
||||
'pos_type_id': {'default_location_src_id': self.lot_stock_id.id}
|
||||
})
|
||||
return picking_type_update_values
|
||||
|
||||
def _get_picking_type_create_values(self, max_sequence):
|
||||
picking_type_create_values, max_sequence = super(Warehouse, self)._get_picking_type_create_values(max_sequence)
|
||||
picking_type_create_values.update({
|
||||
'pos_type_id': {
|
||||
'name': _('PoS Orders'),
|
||||
'code': 'outgoing',
|
||||
'default_location_src_id': self.lot_stock_id.id,
|
||||
'default_location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||||
'sequence': max_sequence + 1,
|
||||
'sequence_code': 'POS',
|
||||
'company_id': self.company_id.id,
|
||||
'show_operations': False,
|
||||
}
|
||||
})
|
||||
return picking_type_create_values, max_sequence + 2
|
||||
|
||||
@api.model
|
||||
def _create_missing_pos_picking_types(self):
|
||||
warehouses = self.env['stock.warehouse'].search([('pos_type_id', '=', False)])
|
||||
for warehouse in warehouses:
|
||||
new_vals = warehouse._create_or_update_sequences_and_picking_types()
|
||||
warehouse.write(new_vals)
|
||||
Loading…
Add table
Add a link
Reference in a new issue