From 2f2bde5d9f647e4785ffabdc1b2e07a08722309c Mon Sep 17 00:00:00 2001 From: Pierre Masereel Date: Thu, 17 Oct 2019 11:24:10 +0000 Subject: [PATCH] [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 --- addons/point_of_sale/models/__init__.py | 1 + addons/point_of_sale/models/account_move.py | 6 +- addons/point_of_sale/models/pos_config.py | 6 +- addons/point_of_sale/models/pos_order.py | 192 +++--------------- addons/point_of_sale/models/pos_session.py | 64 +++++- addons/point_of_sale/models/res_company.py | 6 + .../models/res_config_settings.py | 1 + addons/point_of_sale/models/stock_picking.py | 133 ++++++++++++ .../point_of_sale/models/stock_warehouse.py | 1 + .../point_of_sale/report/pos_order_report.py | 4 +- addons/point_of_sale/static/src/js/models.js | 2 +- addons/point_of_sale/tests/common.py | 6 +- .../point_of_sale/tests/test_anglo_saxon.py | 1 + .../tests/test_point_of_sale_flow.py | 13 +- .../tests/test_pos_basic_config.py | 14 +- .../tests/test_pos_stock_account.py | 8 +- addons/point_of_sale/views/pos_order_view.xml | 13 +- .../point_of_sale/views/pos_session_view.xml | 8 +- .../views/res_config_settings_views.xml | 17 ++ addons/point_of_sale/wizard/pos_payment.py | 1 + 20 files changed, 294 insertions(+), 203 deletions(-) create mode 100644 addons/point_of_sale/models/stock_picking.py 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 @@ +
+ -