[IMP] point_of_sale: create picking after closing session
When you have a lot of pos order to synchronize with Odoo, it may take a lot of time because it has to create a picking for each order. So we've added an option in the settings that will create the picking at the end of the session. So the new option by default is creating the picking at session closure, and if you need a real time stock you can enable the creation of picking for each order. closes odoo/odoo#39584 Task-id: 2082064 Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from . import product
|
||||
from . import res_partner
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import stock_picking
|
||||
from . import stock_warehouse
|
||||
from . import pos_payment
|
||||
from . import pos_payment_method
|
||||
|
||||
@@ -12,9 +12,9 @@ class AccountMove(models.Model):
|
||||
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.type == 'out_invoice'):
|
||||
stock_moves += invoice.sudo().mapped('pos_order_ids.picking_id.move_lines').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer')
|
||||
stock_moves += invoice.sudo().mapped('pos_order_ids.picking_ids.move_lines').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer')
|
||||
for invoice in self.filtered(lambda x: x.type == 'out_refund'):
|
||||
stock_moves += invoice.sudo().mapped('pos_order_ids.picking_id.move_lines').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'customer')
|
||||
stock_moves += invoice.sudo().mapped('pos_order_ids.picking_ids.move_lines').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'customer')
|
||||
return stock_moves
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
@@ -27,5 +27,5 @@ class AccountMoveLine(models.Model):
|
||||
price_unit = super(AccountMoveLine, self)._stock_account_get_anglo_saxon_price_unit()
|
||||
order = self.move_id.pos_order_ids
|
||||
if order:
|
||||
price_unit = - order._get_pos_anglo_saxon_price_unit(self.product_id, self.move_id.partner_id.id, self.quantity)
|
||||
price_unit = order._get_pos_anglo_saxon_price_unit(self.product_id, self.move_id.partner_id.id, self.quantity)
|
||||
return price_unit
|
||||
|
||||
@@ -107,9 +107,9 @@ class PosConfig(models.Model):
|
||||
'stock.picking.type',
|
||||
string='Operation Type',
|
||||
default=_default_picking_type_id,
|
||||
required=True,
|
||||
domain="[('code', '=', 'outgoing'), ('warehouse_id.company_id', '=', company_id)]",
|
||||
ondelete='restrict')
|
||||
use_existing_lots = fields.Boolean(related='picking_type_id.use_existing_lots', readonly=False)
|
||||
journal_id = fields.Many2one(
|
||||
'account.journal', string='Sales Journal',
|
||||
domain=[('type', '=', 'sale')],
|
||||
@@ -510,10 +510,10 @@ class PosConfig(models.Model):
|
||||
# Methods to open the POS
|
||||
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()
|
||||
|
||||
@@ -137,10 +137,10 @@ class PosOrder(models.Model):
|
||||
except Exception as e:
|
||||
_logger.error('Could not fully process the POS Order: %s', tools.ustr(e))
|
||||
|
||||
pos_order._create_order_picking()
|
||||
if pos_order.to_invoice and pos_order.state == 'paid':
|
||||
pos_order.action_pos_order_invoice()
|
||||
pos_order.account_move.sudo().with_context(force_company=self.env.user.company_id.id).post()
|
||||
|
||||
return pos_order.id
|
||||
|
||||
|
||||
@@ -193,11 +193,11 @@ class PosOrder(models.Model):
|
||||
|
||||
def _get_pos_anglo_saxon_price_unit(self, product, partner_id, quantity):
|
||||
moves = self.filtered(lambda o: o.partner_id.id == partner_id)\
|
||||
.mapped('picking_id.move_lines')\
|
||||
.mapped('picking_ids.move_lines')\
|
||||
.filtered(lambda m: m.product_id.id == product.id)\
|
||||
.sorted(lambda x: x.date)
|
||||
price_unit = product._compute_average_price(0, quantity, moves)
|
||||
return - price_unit
|
||||
return price_unit
|
||||
|
||||
name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/')
|
||||
date_order = fields.Datetime(string='Date', readonly=True, index=True, default=fields.Datetime.now)
|
||||
@@ -233,14 +233,11 @@ class PosOrder(models.Model):
|
||||
'Status', readonly=True, copy=False, default='draft')
|
||||
|
||||
account_move = fields.Many2one('account.move', string='Invoice', readonly=True, copy=False)
|
||||
picking_id = fields.Many2one('stock.picking', string='Picking', readonly=True, copy=False)
|
||||
picking_ids = fields.One2many('stock.picking', 'pos_order_id')
|
||||
picking_count = fields.Integer(compute='_compute_picking_count')
|
||||
failed_pickings = fields.Boolean(compute='_compute_picking_count')
|
||||
picking_type_id = fields.Many2one('stock.picking.type', related='session_id.config_id.picking_type_id', string="Operation Type", readonly=False)
|
||||
location_id = fields.Many2one(
|
||||
comodel_name='stock.location',
|
||||
related='picking_id.location_id',
|
||||
string="Location", store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
note = fields.Text(string='Internal Notes')
|
||||
nb_print = fields.Integer(string='Number of Print', readonly=True, copy=False, default=0)
|
||||
pos_reference = fields.Char(string='Receipt Number', readonly=True, copy=False)
|
||||
@@ -261,6 +258,11 @@ class PosOrder(models.Model):
|
||||
for order in self:
|
||||
order.is_invoiced = bool(order.account_move)
|
||||
|
||||
@api.depends('picking_ids', 'picking_ids.state')
|
||||
def _compute_picking_count(self):
|
||||
for order in self:
|
||||
order.picking_count = len(order.picking_ids)
|
||||
order.failed_pickings = bool(order.picking_ids.filtered(lambda p: p.state != 'done'))
|
||||
|
||||
@api.depends('date_order', 'company_id', 'currency_id', 'company_id.currency_id')
|
||||
def _compute_currency_rate(self):
|
||||
@@ -333,6 +335,14 @@ class PosOrder(models.Model):
|
||||
vals['name'] = order.config_id.sequence_id._next()
|
||||
return super(PosOrder, self).write(vals)
|
||||
|
||||
def action_stock_picking(self):
|
||||
self.ensure_one()
|
||||
action_picking = self.env.ref('stock.action_picking_tree_ready')
|
||||
action = action_picking.read()[0]
|
||||
action['context'] = {}
|
||||
action['domain'] = [('id', 'in', self.picking_ids.ids)]
|
||||
return action
|
||||
|
||||
def action_view_invoice(self):
|
||||
return {
|
||||
'name': _('Customer Invoice'),
|
||||
@@ -345,10 +355,11 @@ class PosOrder(models.Model):
|
||||
}
|
||||
|
||||
def action_pos_order_paid(self):
|
||||
self.ensure_one()
|
||||
if not float_is_zero(self.amount_total - self.amount_paid, precision_rounding=self.currency_id.rounding):
|
||||
raise UserError(_("Order %s is not fully paid.") % self.name)
|
||||
self.write({'state': 'paid'})
|
||||
return self.create_picking()
|
||||
return True
|
||||
|
||||
def action_pos_order_invoice(self):
|
||||
moves = self.env['account.move']
|
||||
@@ -427,156 +438,19 @@ class PosOrder(models.Model):
|
||||
|
||||
return self.env['pos.order'].search_read(domain = [('id', 'in', order_ids)], fields = ['id', 'pos_reference'])
|
||||
|
||||
def create_picking(self):
|
||||
"""Create a picking for each order and validate it."""
|
||||
Picking = self.env['stock.picking']
|
||||
# If no email is set on the user, the picking creation and validation will fail be cause of
|
||||
# the 'Unable to log message, please configure the sender's email address.' error.
|
||||
# We disable the tracking in this case.
|
||||
if not self.env.user.partner_id.email:
|
||||
Picking = Picking.with_context(tracking_disable=True)
|
||||
Move = self.env['stock.move']
|
||||
StockWarehouse = self.env['stock.warehouse']
|
||||
for order in self:
|
||||
if not order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']):
|
||||
continue
|
||||
address = order.partner_id.address_get(['delivery']) or {}
|
||||
picking_type = order.picking_type_id
|
||||
return_pick_type = order.picking_type_id.return_picking_type_id or order.picking_type_id
|
||||
order_picking = Picking
|
||||
return_picking = Picking
|
||||
moves = Move
|
||||
location_id = picking_type.default_location_src_id.id
|
||||
if order.partner_id:
|
||||
destination_id = order.partner_id.property_stock_customer.id
|
||||
else:
|
||||
if (not picking_type) or (not picking_type.default_location_dest_id):
|
||||
customerloc, supplierloc = StockWarehouse._get_partner_locations()
|
||||
destination_id = customerloc.id
|
||||
else:
|
||||
destination_id = picking_type.default_location_dest_id.id
|
||||
|
||||
if picking_type:
|
||||
message = _("This transfer has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id=%d>%s</a>") % (order.id, order.name)
|
||||
picking_vals = {
|
||||
'origin': order.name,
|
||||
'partner_id': address.get('delivery', False),
|
||||
'user_id': False,
|
||||
'date_done': order.date_order,
|
||||
'picking_type_id': picking_type.id,
|
||||
'company_id': order.company_id.id,
|
||||
'move_type': 'direct',
|
||||
'note': order.note or "",
|
||||
'location_id': location_id,
|
||||
'location_dest_id': destination_id,
|
||||
}
|
||||
pos_qty = any([x.qty > 0 for x in order.lines if x.product_id.type in ['product', 'consu']])
|
||||
if pos_qty:
|
||||
order_picking = Picking.create(picking_vals.copy())
|
||||
if self.env.user.partner_id.email:
|
||||
order_picking.message_post(body=message)
|
||||
else:
|
||||
order_picking.sudo().message_post(body=message)
|
||||
neg_qty = any([x.qty < 0 for x in order.lines if x.product_id.type in ['product', 'consu']])
|
||||
if neg_qty:
|
||||
return_vals = picking_vals.copy()
|
||||
return_vals.update({
|
||||
'location_id': destination_id,
|
||||
'location_dest_id': return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id,
|
||||
'picking_type_id': return_pick_type.id
|
||||
})
|
||||
return_picking = Picking.create(return_vals)
|
||||
if self.env.user.partner_id.email:
|
||||
return_picking.message_post(body=message)
|
||||
else:
|
||||
return_picking.message_post(body=message)
|
||||
|
||||
for line in order.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)):
|
||||
moves |= Move.create({
|
||||
'name': line.name,
|
||||
'product_uom': line.product_id.uom_id.id,
|
||||
'picking_id': order_picking.id if line.qty >= 0 else return_picking.id,
|
||||
'picking_type_id': picking_type.id if line.qty >= 0 else return_pick_type.id,
|
||||
'product_id': line.product_id.id,
|
||||
'product_uom_qty': abs(line.qty),
|
||||
'state': 'draft',
|
||||
'location_id': location_id if line.qty >= 0 else destination_id,
|
||||
'location_dest_id': destination_id if line.qty >= 0 else return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id,
|
||||
})
|
||||
|
||||
# prefer associating the regular order picking, not the return
|
||||
order.write({'picking_id': order_picking.id or return_picking.id})
|
||||
|
||||
if return_picking:
|
||||
order._force_picking_done(return_picking)
|
||||
if order_picking:
|
||||
order._force_picking_done(order_picking)
|
||||
|
||||
# when the pos.config has no picking_type_id set only the moves will be created
|
||||
if moves and not return_picking and not order_picking:
|
||||
moves._action_assign()
|
||||
moves.filtered(lambda m: m.product_id.tracking == 'none')._action_done()
|
||||
|
||||
return True
|
||||
|
||||
def _force_picking_done(self, picking):
|
||||
"""Force picking in order to be set as done."""
|
||||
def _create_order_picking(self):
|
||||
self.ensure_one()
|
||||
picking.action_assign()
|
||||
wrong_lots = self.set_pack_operation_lot(picking)
|
||||
if not wrong_lots:
|
||||
picking._action_done()
|
||||
if not self.session_id.update_stock_at_closing or (self.company_id.anglo_saxon_accounting and self.to_invoice):
|
||||
picking_type = self.config_id.picking_type_id
|
||||
if self.partner_id.property_stock_customer:
|
||||
destination_id = self.partner_id.property_stock_customer.id
|
||||
elif not picking_type or not picking_type.default_location_dest_id:
|
||||
destination_id = self.env['stock.warehouse']._get_partner_locations()[0].id
|
||||
else:
|
||||
destination_id = picking_type.default_location_dest_id.id
|
||||
|
||||
def set_pack_operation_lot(self, picking=None):
|
||||
"""Set Serial/Lot number in pack operations to mark the pack operation done."""
|
||||
|
||||
StockProductionLot = self.env['stock.production.lot']
|
||||
PosPackOperationLot = self.env['pos.pack.operation.lot']
|
||||
has_wrong_lots = False
|
||||
for order in self:
|
||||
for move in (picking or self.picking_id).move_lines:
|
||||
picking_type = (picking or self.picking_id).picking_type_id
|
||||
lots_necessary = True
|
||||
if picking_type:
|
||||
lots_necessary = picking_type and picking_type.use_existing_lots
|
||||
qty_done = 0
|
||||
pack_lots = []
|
||||
pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', move.product_id.id)])
|
||||
|
||||
if pos_pack_lots and lots_necessary:
|
||||
for pos_pack_lot in pos_pack_lots:
|
||||
stock_production_lot = StockProductionLot.search([('name', '=', pos_pack_lot.lot_name), ('product_id', '=', move.product_id.id)])
|
||||
if stock_production_lot:
|
||||
# a serialnumber always has a quantity of 1 product, a lot number takes the full quantity of the order line
|
||||
qty = 1.0
|
||||
if stock_production_lot.product_id.tracking == 'lot':
|
||||
qty = abs(pos_pack_lot.pos_order_line_id.qty)
|
||||
qty_done += qty
|
||||
pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty})
|
||||
else:
|
||||
has_wrong_lots = True
|
||||
elif move.product_id.tracking == 'none' or not lots_necessary:
|
||||
qty_done = move.product_uom_qty
|
||||
else:
|
||||
has_wrong_lots = True
|
||||
for pack_lot in pack_lots:
|
||||
lot_id, qty = pack_lot['lot_id'], pack_lot['qty']
|
||||
self.env['stock.move.line'].create({
|
||||
'picking_id': move.picking_id.id,
|
||||
'move_id': move.id,
|
||||
'product_id': move.product_id.id,
|
||||
'product_uom_id': move.product_uom.id,
|
||||
'qty_done': qty,
|
||||
'location_id': move.location_id.id,
|
||||
'location_dest_id': move.location_dest_id.id,
|
||||
'lot_id': lot_id,
|
||||
})
|
||||
if not pack_lots and not float_is_zero(qty_done, precision_rounding=move.product_uom.rounding):
|
||||
if len(move._get_move_lines()) < 2:
|
||||
move.quantity_done = qty_done
|
||||
else:
|
||||
move._set_quantity_done(qty_done)
|
||||
return has_wrong_lots
|
||||
pickings = self.env['stock.picking']._create_picking_from_pos_order_lines(destination_id, self.lines, picking_type, self.partner_id)
|
||||
pickings.write({'pos_session_id': self.session_id.id, 'pos_order_id': self.id, 'origin': self.name})
|
||||
|
||||
def add_payment(self, data):
|
||||
"""Create a new payment for the order"""
|
||||
|
||||
@@ -84,7 +84,9 @@ class PosSession(models.Model):
|
||||
order_ids = fields.One2many('pos.order', 'session_id', string='Orders')
|
||||
order_count = fields.Integer(compute='_compute_order_count')
|
||||
statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Cash Statements', readonly=True)
|
||||
failed_pickings = fields.Boolean(compute='_compute_picking_count')
|
||||
picking_count = fields.Integer(compute='_compute_picking_count')
|
||||
picking_ids = fields.One2many('stock.picking', 'pos_session_id')
|
||||
rescue = fields.Boolean(string='Recovery Session',
|
||||
help="Auto-generated session for orphan orders, ignored in constraints",
|
||||
readonly=True,
|
||||
@@ -93,6 +95,7 @@ class PosSession(models.Model):
|
||||
payment_method_ids = fields.Many2many('pos.payment.method', related='config_id.payment_method_ids', string='Payment Methods')
|
||||
total_payments_amount = fields.Float(compute='_compute_total_payments_amount', string='Total Payments Amount')
|
||||
is_in_company_currency = fields.Boolean('Is Using Company Currency', compute='_compute_is_in_company_currency')
|
||||
update_stock_at_closing = fields.Boolean('Stock should be updated at closing')
|
||||
|
||||
_sql_constraints = [('uniq_name', 'unique(name)', "The name of this POS Session must be unique !")]
|
||||
|
||||
@@ -128,17 +131,18 @@ class PosSession(models.Model):
|
||||
for session in self:
|
||||
session.order_count = sessions_data.get(session.id, 0)
|
||||
|
||||
@api.depends('picking_ids', 'picking_ids.state')
|
||||
def _compute_picking_count(self):
|
||||
for pos in self:
|
||||
pickings = pos.order_ids.mapped('picking_id').filtered(lambda x: x.state != 'done')
|
||||
pos.picking_count = len(pickings.ids)
|
||||
for session in self:
|
||||
session.picking_count = len(session.picking_ids.ids)
|
||||
session.failed_pickings = bool(session.picking_ids.filtered(lambda p: p.state != 'done'))
|
||||
|
||||
def action_stock_picking(self):
|
||||
pickings = self.order_ids.mapped('picking_id').filtered(lambda x: x.state != 'done')
|
||||
self.ensure_one()
|
||||
action_picking = self.env.ref('stock.action_picking_tree_ready')
|
||||
action = action_picking.read()[0]
|
||||
action['context'] = {}
|
||||
action['domain'] = [('id', 'in', pickings.ids)]
|
||||
action['domain'] = [('id', 'in', self.picking_ids.ids)]
|
||||
return action
|
||||
|
||||
@api.depends('config_id', 'statement_ids', 'payment_method_ids')
|
||||
@@ -204,10 +208,13 @@ class PosSession(models.Model):
|
||||
}
|
||||
statement_ids |= statement_ids.with_context(ctx).create(st_values)
|
||||
|
||||
update_stock_at_closing = pos_config.company_id.point_of_sale_update_stock_quantities == "closing"
|
||||
|
||||
values.update({
|
||||
'name': pos_name,
|
||||
'statement_ids': [(6, 0, statement_ids.ids)],
|
||||
'config_id': config_id,
|
||||
'update_stock_at_closing': update_stock_at_closing,
|
||||
})
|
||||
|
||||
if self.user_has_groups('point_of_sale.group_pos_user'):
|
||||
@@ -283,6 +290,8 @@ class PosSession(models.Model):
|
||||
def _validate_session(self):
|
||||
self.ensure_one()
|
||||
self._check_if_no_draft_orders()
|
||||
if self.update_stock_at_closing:
|
||||
self._create_picking_at_end_of_session()
|
||||
self._create_account_move()
|
||||
if self.move_id.line_ids:
|
||||
self.move_id.post()
|
||||
@@ -302,6 +311,30 @@ class PosSession(models.Model):
|
||||
'params': {'menu_id': self.env.ref('point_of_sale.menu_point_root').id},
|
||||
}
|
||||
|
||||
def _create_picking_at_end_of_session(self):
|
||||
self.ensure_one()
|
||||
lines_grouped_by_dest_location = {}
|
||||
picking_type = self.config_id.picking_type_id
|
||||
|
||||
if not picking_type or not picking_type.default_location_dest_id:
|
||||
session_destination_id = self.env['stock.warehouse']._get_partner_locations()[0].id
|
||||
else:
|
||||
session_destination_id = picking_type.default_location_dest_id.id
|
||||
|
||||
for order in self.order_ids:
|
||||
if order.company_id.anglo_saxon_accounting and order.to_invoice:
|
||||
continue
|
||||
destination_id = order.partner_id.property_stock_customer.id or session_destination_id
|
||||
if destination_id in lines_grouped_by_dest_location:
|
||||
lines_grouped_by_dest_location[destination_id] |= order.lines
|
||||
else:
|
||||
lines_grouped_by_dest_location[destination_id] = order.lines
|
||||
|
||||
for location_dest_id, lines in lines_grouped_by_dest_location.items():
|
||||
pickings = self.env['stock.picking']._create_picking_from_pos_order_lines(location_dest_id, lines, picking_type)
|
||||
pickings.write({'pos_session_id': self.id, 'origin': self.name})
|
||||
|
||||
|
||||
def _create_account_move(self):
|
||||
""" Create account.move and account.move.line records for this session.
|
||||
|
||||
@@ -397,7 +430,7 @@ class PosSession(models.Model):
|
||||
if self.company_id.anglo_saxon_accounting:
|
||||
# Combine stock lines
|
||||
stock_moves = self.env['stock.move'].search([
|
||||
('picking_id', '=', order.picking_id.id),
|
||||
('picking_id', 'in', order.picking_ids.ids),
|
||||
('company_id.anglo_saxon_accounting', '=', True),
|
||||
('product_id.categ_id.property_valuation', '=', 'real_time')
|
||||
])
|
||||
@@ -408,6 +441,21 @@ class PosSession(models.Model):
|
||||
stock_expense[exp_key] = self._update_amounts(stock_expense[exp_key], {'amount': amount}, move.picking_id.date)
|
||||
stock_output[out_key] = self._update_amounts(stock_output[out_key], {'amount': amount}, move.picking_id.date)
|
||||
|
||||
if self.company_id.anglo_saxon_accounting:
|
||||
global_session_pickings = self.picking_ids.filtered(lambda p: not p.pos_order_id)
|
||||
if global_session_pickings:
|
||||
stock_moves = self.env['stock.move'].search([
|
||||
('picking_id', 'in', global_session_pickings.ids),
|
||||
('company_id.anglo_saxon_accounting', '=', True),
|
||||
('product_id.categ_id.property_valuation', '=', 'real_time'),
|
||||
])
|
||||
for move in stock_moves:
|
||||
exp_key = move.product_id.property_account_expense_id or move.product_id.categ_id.property_account_expense_categ_id
|
||||
out_key = move.product_id.categ_id.property_stock_account_output_categ_id
|
||||
amount = -sum(move.stock_valuation_layer_ids.mapped('value'))
|
||||
stock_expense[exp_key] = self._update_amounts(stock_expense[exp_key], {'amount': amount}, move.picking_id.date)
|
||||
stock_output[out_key] = self._update_amounts(stock_output[out_key], {'amount': amount}, move.picking_id.date)
|
||||
|
||||
## SECTION: Create non-reconcilable move lines
|
||||
# Create account.move.line records for
|
||||
# - sales
|
||||
@@ -514,7 +562,9 @@ class PosSession(models.Model):
|
||||
).reconcile()
|
||||
|
||||
# reconcile stock output lines
|
||||
stock_moves = self.env['stock.move'].search([('picking_id', 'in', self.order_ids.filtered(lambda order: not order.is_invoiced).mapped('picking_id').ids)])
|
||||
pickings = self.picking_ids.filtered(lambda p: not p.pos_order_id)
|
||||
pickings |= self.order_ids.filtered(lambda o: not o.is_invoiced).mapped('picking_ids')
|
||||
stock_moves = self.env['stock.move'].search([('picking_id', 'in', pickings.ids)])
|
||||
stock_account_move_lines = self.env['account.move'].search([('stock_move_id', 'in', stock_moves.ids)]).mapped('line_ids')
|
||||
for account_id in stock_output_lines:
|
||||
( stock_output_lines[account_id]
|
||||
|
||||
@@ -6,6 +6,12 @@ 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 (advised)'),
|
||||
('real', 'In real time'),
|
||||
], 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")
|
||||
|
||||
@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
|
||||
|
||||
@@ -9,3 +9,4 @@ class ResConfigSettings(models.TransientModel):
|
||||
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.")
|
||||
update_stock_quantities = fields.Selection(related="company_id.point_of_sale_update_stock_quantities", readonly=False)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- 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
|
||||
|
||||
from itertools import groupby
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit='stock.picking'
|
||||
|
||||
pos_session_id = fields.Many2one('pos.session')
|
||||
pos_order_id = fields.Many2one('pos.order')
|
||||
|
||||
|
||||
@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({
|
||||
'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,
|
||||
})
|
||||
|
||||
positive_picking._create_move_from_pos_order_lines(positive_lines)
|
||||
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({
|
||||
'partner_id': partner.id if partner else False,
|
||||
'user_id': False,
|
||||
'picking_type_id': return_picking_type.id,
|
||||
'move_type': 'direct',
|
||||
'location_id': location_dest_id,
|
||||
'location_dest_id': return_location_id,
|
||||
})
|
||||
negative_picking._create_move_from_pos_order_lines(negative_lines)
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
negative_picking._action_done()
|
||||
except (UserError, ValidationError):
|
||||
pass
|
||||
pickings |= negative_picking
|
||||
return pickings
|
||||
|
||||
|
||||
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)
|
||||
for product, lines in lines_by_product:
|
||||
order_lines = self.env['pos.order.line'].concat(*lines)
|
||||
first_line = order_lines[0]
|
||||
current_move = self.env['stock.move'].create({
|
||||
'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,
|
||||
})
|
||||
if first_line.product_id.tracking != 'none' and (self.picking_type_id.use_existing_lots or self.picking_type_id.use_create_lots):
|
||||
for line in 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 = current_move._prepare_move_line_vals()
|
||||
ml_vals.update({'qty_done':qty})
|
||||
if self.picking_type_id.use_existing_lots:
|
||||
existing_lot = self.env['stock.production.lot'].search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('product_id', '=', line.product_id.id),
|
||||
('name', '=', lot.lot_name)
|
||||
])
|
||||
if not existing_lot and self.picking_type_id.use_create_lots:
|
||||
existing_lot = self.env['stock.production.lot'].create({
|
||||
'company_id': self.company_id.id,
|
||||
'product_id': line.product_id.id,
|
||||
'name': lot.lot_name,
|
||||
})
|
||||
ml_vals.update({
|
||||
'lot_id': existing_lot.id,
|
||||
})
|
||||
else:
|
||||
ml_vals.update({
|
||||
'lot_name': lot.lot_name,
|
||||
})
|
||||
self.env['stock.move.line'].create(ml_vals)
|
||||
sum_of_lots += qty
|
||||
if abs(line.qty) != sum_of_lots:
|
||||
difference_qty = abs(line.qty) - sum_of_lots
|
||||
ml_vals = current_move._prepare_move_line_vals()
|
||||
if line.product_id.tracking == 'serial':
|
||||
ml_vals.update({'qty_done': 1})
|
||||
for i in range(int(difference_qty)):
|
||||
self.env['stock.move.line'].create(ml_vals)
|
||||
else:
|
||||
ml_vals.update({'qty_done': difference_qty})
|
||||
self.env['stock.move.line'].create(ml_vals)
|
||||
else:
|
||||
current_move.quantity_done = abs(sum(order_lines.mapped('qty')))
|
||||
@@ -38,6 +38,7 @@ class Warehouse(models.Model):
|
||||
'sequence': max_sequence + 1,
|
||||
'sequence_code': 'POS',
|
||||
'company_id': self.company_id.id,
|
||||
'show_operations': False,
|
||||
}
|
||||
})
|
||||
return picking_type_create_values, max_sequence + 2
|
||||
|
||||
@@ -24,7 +24,6 @@ class PosOrderReport(models.Model):
|
||||
price_sub_total = fields.Float(string='Subtotal w/o discount', readonly=True)
|
||||
total_discount = fields.Float(string='Total Discount', readonly=True)
|
||||
average_price = fields.Float(string='Average Price', readonly=True, group_operator="avg")
|
||||
location_id = fields.Many2one('stock.location', string='Location', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True)
|
||||
nbr_lines = fields.Integer(string='Sale Line Count', readonly=True)
|
||||
product_qty = fields.Integer(string='Product Quantity', readonly=True)
|
||||
@@ -53,7 +52,6 @@ class PosOrderReport(models.Model):
|
||||
s.partner_id AS partner_id,
|
||||
s.state AS state,
|
||||
s.user_id AS user_id,
|
||||
s.location_id AS location_id,
|
||||
s.company_id AS company_id,
|
||||
s.sale_journal AS journal_id,
|
||||
l.product_id AS product_id,
|
||||
@@ -80,7 +78,7 @@ class PosOrderReport(models.Model):
|
||||
return """
|
||||
GROUP BY
|
||||
s.id, s.date_order, s.partner_id,s.state, pt.categ_id,
|
||||
s.user_id, s.location_id, s.company_id, s.sale_journal,
|
||||
s.user_id, s.company_id, s.sale_journal,
|
||||
s.pricelist_id, s.account_move, s.create_date, s.session_id,
|
||||
l.product_id,
|
||||
pt.categ_id, pt.pos_categ_id,
|
||||
|
||||
@@ -1483,7 +1483,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
return orderline;
|
||||
},
|
||||
set_product_lot: function(product){
|
||||
this.has_product_lot = product.tracking !== 'none' && this.pos.config.use_existing_lots;
|
||||
this.has_product_lot = product.tracking !== 'none';
|
||||
this.pack_lot_lines = this.has_product_lot && new PacklotlineCollection(null, {'order_line': this});
|
||||
},
|
||||
// sets a discount [0,100]%
|
||||
|
||||
@@ -19,6 +19,7 @@ class TestPointOfSaleCommon(StockAccountMinimalTest):
|
||||
cls.PosOrder = cls.env['pos.order']
|
||||
cls.PosSession = cls.env['pos.session']
|
||||
cls.company = cls.env.ref('base.main_company')
|
||||
cls.company.point_of_sale_update_stock_quantities = 'real'
|
||||
cls.company_id = cls.company.id
|
||||
coa = cls.env['account.chart.template'].search([
|
||||
('currency_id', '=', cls.company.currency_id.id),
|
||||
@@ -162,6 +163,7 @@ class TestPoSCommon(StockAccountMinimalTest):
|
||||
|
||||
# Set basic defaults
|
||||
cls.company = cls.env.ref('base.main_company')
|
||||
cls.company.point_of_sale_update_stock_quantities = 'real'
|
||||
cls.pos_sale_journal = cls.env['account.journal'].create({
|
||||
'type': 'sale',
|
||||
'name': 'Point of Sale Test',
|
||||
@@ -183,8 +185,8 @@ class TestPoSCommon(StockAccountMinimalTest):
|
||||
'name': 'Debtors - (POS)',
|
||||
'reconcile': True,
|
||||
'user_type_id': cls.env.ref('account.data_account_type_receivable').id,
|
||||
})
|
||||
cls.pos_receivable_account = cls.company.account_default_pos_receivable_account_id
|
||||
})
|
||||
cls.pos_receivable_account = cls.company.account_default_pos_receivable_account_id
|
||||
cls.other_receivable_account = cls.env['account.account'].create({
|
||||
'name': 'Other Receivable',
|
||||
'code': 'RCV00' ,
|
||||
|
||||
@@ -42,6 +42,7 @@ class TestAngloSaxonCommon(common.TransactionCase):
|
||||
'type': 'product',
|
||||
})
|
||||
self.company.anglo_saxon_accounting = True
|
||||
self.company.point_of_sale_update_stock_quantities = 'real'
|
||||
self.product.categ_id = self.category
|
||||
self.product.property_account_expense_id = account_expense
|
||||
self.product.property_account_income_id = account_income
|
||||
|
||||
@@ -155,12 +155,12 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
# I test that the pickings are created as expected during payment
|
||||
# One picking attached and having all the positive move lines in the correct state
|
||||
self.assertEqual(
|
||||
self.pos_order_pos1.picking_id.state,
|
||||
self.pos_order_pos1.picking_ids[0].state,
|
||||
'done',
|
||||
'Picking should be in done state.'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.pos_order_pos1.picking_id.move_lines.mapped('state'),
|
||||
self.pos_order_pos1.picking_ids[0].move_lines.mapped('state'),
|
||||
['done', 'done'],
|
||||
'Move Lines should be in done state.'
|
||||
)
|
||||
@@ -219,14 +219,13 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
|
||||
# I test that the pickings are created as expected
|
||||
# One picking attached and having all the positive move lines in the correct state
|
||||
self.pos_order_pos2.create_picking()
|
||||
self.assertEqual(
|
||||
self.pos_order_pos2.picking_id.state,
|
||||
self.pos_order_pos2.picking_ids[0].state,
|
||||
'done',
|
||||
'Picking should be in done state.'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.pos_order_pos2.picking_id.move_lines.mapped('state'),
|
||||
self.pos_order_pos2.picking_ids[0].move_lines.mapped('state'),
|
||||
['done', 'done'],
|
||||
'Move Lines should be in done state.'
|
||||
)
|
||||
@@ -285,12 +284,12 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
# I test that the pickings are created as expected
|
||||
# One picking attached and having all the positive move lines in the correct state
|
||||
self.assertEqual(
|
||||
self.pos_order_pos3.picking_id.state,
|
||||
self.pos_order_pos3.picking_ids[0].state,
|
||||
'done',
|
||||
'Picking should be in done state.'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.pos_order_pos3.picking_id.move_lines.mapped('state'),
|
||||
self.pos_order_pos3.picking_ids[0].move_lines.mapped('state'),
|
||||
['done'],
|
||||
'Move Lines should be in done state.'
|
||||
)
|
||||
|
||||
@@ -100,11 +100,11 @@ class TestPoSBasicConfig(TestPoSCommon):
|
||||
# picking and stock moves should be in done state
|
||||
for order in self.pos_session.order_ids:
|
||||
self.assertEqual(
|
||||
order.picking_id.state,
|
||||
order.picking_ids[0].state,
|
||||
'done',
|
||||
'Picking should be in done state.'
|
||||
)
|
||||
move_lines = order.picking_id.move_lines
|
||||
move_lines = order.picking_ids[0].move_lines
|
||||
self.assertEqual(
|
||||
move_lines.mapped('state'),
|
||||
['done'] * len(move_lines),
|
||||
@@ -216,11 +216,11 @@ class TestPoSBasicConfig(TestPoSCommon):
|
||||
# no exception for invoiced orders
|
||||
for order in self.pos_session.order_ids:
|
||||
self.assertEqual(
|
||||
order.picking_id.state,
|
||||
order.picking_ids[0].state,
|
||||
'done',
|
||||
'Picking should be in done state.'
|
||||
)
|
||||
move_lines = order.picking_id.move_lines
|
||||
move_lines = order.picking_ids[0].move_lines
|
||||
self.assertEqual(
|
||||
move_lines.mapped('state'),
|
||||
['done'] * len(move_lines),
|
||||
@@ -376,11 +376,11 @@ class TestPoSBasicConfig(TestPoSCommon):
|
||||
# no exception of return orders
|
||||
for order in self.pos_session.order_ids:
|
||||
self.assertEqual(
|
||||
order.picking_id.state,
|
||||
order.picking_ids[0].state,
|
||||
'done',
|
||||
'Picking should be in done state.'
|
||||
)
|
||||
move_lines = order.picking_id.move_lines
|
||||
move_lines = order.picking_ids[0].move_lines
|
||||
self.assertEqual(
|
||||
move_lines.mapped('state'),
|
||||
['done'] * len(move_lines),
|
||||
@@ -440,4 +440,4 @@ class TestPoSBasicConfig(TestPoSCommon):
|
||||
self.assertAlmostEqual(sum(cash_receivable_lines.mapped('balance')), 290, msg='Total cash receivable balance should be equal to the total cash payments.')
|
||||
|
||||
for line in cash_receivable_lines:
|
||||
self.assertTrue(line.full_reconcile_id, msg='Each cash receivable line should be fully-reconciled.')
|
||||
self.assertTrue(line.full_reconcile_id, msg='Each cash receivable line should be fully-reconciled.')
|
||||
|
||||
@@ -91,8 +91,8 @@ class TestPoSStock(TestPoSCommon):
|
||||
|
||||
# picking and stock moves should be in done state
|
||||
for order in self.pos_session.order_ids:
|
||||
self.assertEqual(order.picking_id.state, 'done', 'Picking should be in done state.')
|
||||
self.assertTrue(all(state == 'done' for state in order.picking_id.move_lines.mapped('state')), 'Move Lines should be in done state.' )
|
||||
self.assertEqual(order.picking_ids[0].state, 'done', 'Picking should be in done state.')
|
||||
self.assertTrue(all(state == 'done' for state in order.picking_ids[0].move_lines.mapped('state')), 'Move Lines should be in done state.' )
|
||||
|
||||
# close the session
|
||||
self.pos_session.action_pos_session_validate()
|
||||
@@ -166,8 +166,8 @@ class TestPoSStock(TestPoSCommon):
|
||||
|
||||
# picking and stock moves should be in done state
|
||||
for order in self.pos_session.order_ids:
|
||||
self.assertEqual(order.picking_id.state, 'done', 'Picking should be in done state.')
|
||||
self.assertTrue(all(state == 'done' for state in order.picking_id.move_lines.mapped('state')), 'Move Lines should be in done state.' )
|
||||
self.assertEqual(order.picking_ids[0].state, 'done', 'Picking should be in done state.')
|
||||
self.assertTrue(all(state == 'done' for state in order.picking_ids[0].move_lines.mapped('state')), 'Move Lines should be in done state.' )
|
||||
|
||||
# close the session
|
||||
self.pos_session.action_pos_session_validate()
|
||||
|
||||
@@ -14,7 +14,16 @@
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,paid,done" />
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="failed_pickings" invisible="1"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_stock_picking"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-truck"
|
||||
attrs="{'invisible':[('picking_count', '=', 0)]}">
|
||||
<field name="picking_count" widget="statinfo" string="Pickings" attrs="{'invisible': [('failed_pickings', '!=', False)]}"/>
|
||||
<field name="picking_count" widget="statinfo" string="Pickings" class="text-danger" attrs="{'invisible': [('failed_pickings', '=', False)]}"/>
|
||||
</button>
|
||||
<button name="action_view_invoice"
|
||||
string="Invoice"
|
||||
type="object"
|
||||
@@ -90,10 +99,6 @@
|
||||
</page>
|
||||
<page name="extra" string="Extra Info">
|
||||
<group >
|
||||
<group string="Inventory">
|
||||
<field name="location_id" widget="selection" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="picking_id" readonly="1"/>
|
||||
</group>
|
||||
<group
|
||||
string="Accounting"
|
||||
groups="account.group_account_manager"
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="cash_register_id" invisible="1"/>
|
||||
<field name="failed_pickings" invisible="1"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_order"
|
||||
class="oe_stat_button"
|
||||
@@ -33,8 +34,9 @@
|
||||
type="object">
|
||||
<field name="order_count" widget="statinfo" string="Orders"/>
|
||||
</button>
|
||||
<button class="oe_stat_button" name="action_stock_picking" type="object" icon="fa-arrows-v" attrs="{'invisible':[('picking_count', '=', 0)]}">
|
||||
<field name="picking_count" widget="statinfo" string="Picking Errors"/>
|
||||
<button class="oe_stat_button" name="action_stock_picking" type="object" icon="fa-truck" attrs="{'invisible':[('picking_count', '=', 0)]}">
|
||||
<field name="picking_count" widget="statinfo" string="Pickings" attrs="{'invisible': [('failed_pickings', '!=', False)]}"/>
|
||||
<field name="picking_count" widget="statinfo" string="Pickings" class="text-danger" attrs="{'invisible': [('failed_pickings', '=', False)]}"/>
|
||||
</button>
|
||||
<button
|
||||
name="action_show_payments_list"
|
||||
@@ -180,7 +182,7 @@
|
||||
<field name="config_id" />
|
||||
<field name="user_id" />
|
||||
<filter name="my_sessions" string="My Sessions" domain="[('user_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<separator/>
|
||||
<filter name="open_sessions" string="In Progress" domain="[('state', '=', 'opened')]"/>
|
||||
<separator/>
|
||||
<filter string="Opening Date" name="start_date" date="start_at" />
|
||||
|
||||
@@ -8,6 +8,23 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[hasclass('settings')]" position="inside">
|
||||
<div class="app_settings_block" data-string="Point of sale" string="Point of Sale" data-key="point_of_sale" groups="point_of_sale.group_pos_manager">
|
||||
<h2>Product</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<div>
|
||||
<label string="Inventory Management" for="update_stock_quantities"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
|
||||
<div class="text-muted">
|
||||
Update quantities in stock
|
||||
</div>
|
||||
<div class="content-group mt16">
|
||||
<field name="update_stock_quantities" colspan="4" nolabel="1" widget="radio"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Taxes</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box" title="This tax is applied to any new product created in the catalog.">
|
||||
|
||||
@@ -56,6 +56,7 @@ class PosMakePayment(models.TransientModel):
|
||||
|
||||
if float_is_zero(order.amount_total - order.amount_paid, precision_rounding=currency.rounding):
|
||||
order.action_pos_order_paid()
|
||||
order._create_order_picking()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
return self.launch_payment()
|
||||
|
||||
Reference in New Issue
Block a user