diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py index bde0f6f3819..f6ec809e5ce 100644 --- a/addons/point_of_sale/models/__init__.py +++ b/addons/point_of_sale/models/__init__.py @@ -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 diff --git a/addons/point_of_sale/models/account_move.py b/addons/point_of_sale/models/account_move.py index 9097267a641..dba398b7966 100644 --- a/addons/point_of_sale/models/account_move.py +++ b/addons/point_of_sale/models/account_move.py @@ -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 diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index eb4f3f0d64a..a2104801ef4 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -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() diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index d3ba047c643..178589f2b7a 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -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: %s") % (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""" diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 2d802f45ffb..c5cfe503200 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -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] diff --git a/addons/point_of_sale/models/res_company.py b/addons/point_of_sale/models/res_company.py index 64aa3646347..ea778a88d15 100644 --- a/addons/point_of_sale/models/res_company.py +++ b/addons/point_of_sale/models/res_company.py @@ -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 diff --git a/addons/point_of_sale/models/res_config_settings.py b/addons/point_of_sale/models/res_config_settings.py index f1fcf4db6d4..07b2346b75a 100644 --- a/addons/point_of_sale/models/res_config_settings.py +++ b/addons/point_of_sale/models/res_config_settings.py @@ -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) diff --git a/addons/point_of_sale/models/stock_picking.py b/addons/point_of_sale/models/stock_picking.py new file mode 100644 index 00000000000..c9d631ee4c6 --- /dev/null +++ b/addons/point_of_sale/models/stock_picking.py @@ -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'))) diff --git a/addons/point_of_sale/models/stock_warehouse.py b/addons/point_of_sale/models/stock_warehouse.py index b5aabdc2658..f0058b32c2f 100644 --- a/addons/point_of_sale/models/stock_warehouse.py +++ b/addons/point_of_sale/models/stock_warehouse.py @@ -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 diff --git a/addons/point_of_sale/report/pos_order_report.py b/addons/point_of_sale/report/pos_order_report.py index e49cd20e9c0..21eb73d8510 100644 --- a/addons/point_of_sale/report/pos_order_report.py +++ b/addons/point_of_sale/report/pos_order_report.py @@ -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, diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index ed9c92b1f3c..6a47c704efb 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -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]% diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index 6750acb8580..d9db5cf5620 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -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' , diff --git a/addons/point_of_sale/tests/test_anglo_saxon.py b/addons/point_of_sale/tests/test_anglo_saxon.py index fb3d8b031e3..2d22a9d5fef 100644 --- a/addons/point_of_sale/tests/test_anglo_saxon.py +++ b/addons/point_of_sale/tests/test_anglo_saxon.py @@ -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 diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index f6e24c74b31..c433ebf7b53 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -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.' ) diff --git a/addons/point_of_sale/tests/test_pos_basic_config.py b/addons/point_of_sale/tests/test_pos_basic_config.py index 3957c5e5fab..607afafa47f 100644 --- a/addons/point_of_sale/tests/test_pos_basic_config.py +++ b/addons/point_of_sale/tests/test_pos_basic_config.py @@ -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.') \ No newline at end of file + self.assertTrue(line.full_reconcile_id, msg='Each cash receivable line should be fully-reconciled.') diff --git a/addons/point_of_sale/tests/test_pos_stock_account.py b/addons/point_of_sale/tests/test_pos_stock_account.py index 31d4b9602db..a40171bd0ec 100644 --- a/addons/point_of_sale/tests/test_pos_stock_account.py +++ b/addons/point_of_sale/tests/test_pos_stock_account.py @@ -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() diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml index dfb4b9fc5d8..c7636b4da90 100644 --- a/addons/point_of_sale/views/pos_order_view.xml +++ b/addons/point_of_sale/views/pos_order_view.xml @@ -14,7 +14,16 @@ +
+ -