diff --git a/stock_force_date/__init__.py b/stock_force_date/__init__.py new file mode 100644 index 000000000..befdc6906 --- /dev/null +++ b/stock_force_date/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import models diff --git a/stock_force_date/__manifest__.py b/stock_force_date/__manifest__.py new file mode 100644 index 000000000..2f90d2323 --- /dev/null +++ b/stock_force_date/__manifest__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# Copyright (C) 2009-TODAY Cybrosys Technologies(). +# Author: fasluca() +# you can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# It is forbidden to publish, distribute, sublicense, or sell copies +# of the Software or modified copies of the Software. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# GENERAL PUBLIC LICENSE (LGPL v3) along with this program. +# If not, see . +# +############################################################################## +{ + 'name': 'Stock Force Date', + 'version': '10.0.1.0', + 'summary': 'Force Date in Stock Picking', + 'description': """ + This module will give you a way to record stock picking to a specific date. + this will effect on related stock quants, moves and stock journal entries. + """, + 'author': 'Cybrosys Techno Solutions', + 'company': 'Cybrosys Techno Solutions', + 'website': "https://cybrosys.com/", + 'category': 'Warehouse', + 'depends': ['stock', 'stock_account'], + 'data': [ + 'views/stock_view.xml', + ], + 'demo': [], + 'images': ['static/description/banner.jpg'], + 'license': 'LGPL-3', + 'installable': True, + 'application': False +} diff --git a/stock_force_date/models/__init__.py b/stock_force_date/models/__init__.py new file mode 100644 index 000000000..daeac082e --- /dev/null +++ b/stock_force_date/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import stock diff --git a/stock_force_date/models/stock.py b/stock_force_date/models/stock.py new file mode 100644 index 000000000..63b04456a --- /dev/null +++ b/stock_force_date/models/stock.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +import time +import logging +from datetime import datetime +from collections import defaultdict +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + +_logger = logging.getLogger(__name__) + + +class Picking(models.Model): + _inherit = "stock.picking" + + force_date = fields.Datetime('Force Date') + + +class Quant(models.Model): + _inherit = "stock.quant" + + @api.model + def quants_move(self, quants, move, location_to, location_from=False, lot_id=False, owner_id=False, + src_package_id=False, dest_package_id=False, entire_pack=False): + """Moves all given stock.quant in the given destination location. Unreserve from current move. + :param quants: list of tuple(browse record(stock.quant) or None, quantity to move) + :param move: browse record (stock.move) + :param location_to: browse record (stock.location) depicting where the quants have to be moved + :param location_from: optional browse record (stock.location) explaining where the quant has to be taken + (may differ from the move source location in case a removal strategy applied). + This parameter is only used to pass to _quant_create_from_move if a negative quant must be created + :param lot_id: ID of the lot that must be set on the quants to move + :param owner_id: ID of the partner that must own the quants to move + :param src_package_id: ID of the package that contains the quants to move + :param dest_package_id: ID of the package that must be set on the moved quant + """ + # TDE CLEANME: use ids + quantities dict + if location_to.usage == 'view': + raise UserError(_('You cannot move to a location of type view %s.') % (location_to.name)) + + quants_reconcile_sudo = self.env['stock.quant'].sudo() + quants_move_sudo = self.env['stock.quant'].sudo() + check_lot = False + for quant, qty in quants: + if not quant: + # If quant is None, we will create a quant to move (and potentially a negative counterpart too) + quant = self._quant_create_from_move( + qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, + dest_package_id=dest_package_id, force_location_from=location_from, force_location_to=location_to) + if move.picking_id.force_date: + quant.write({'in_date': move.picking_id.force_date}) + check_lot = True + else: + _logger.info(quant) + quant._quant_split(qty) + _logger.info(quant) + quants_move_sudo |= quant + quants_reconcile_sudo |= quant + + if quants_move_sudo: + moves_recompute = quants_move_sudo.filtered(lambda self: self.reservation_id != move).mapped( + 'reservation_id') + quants_move_sudo._quant_update_from_move(move, location_to, dest_package_id, lot_id=lot_id, + entire_pack=entire_pack) + moves_recompute.recalculate_move_state() + + if location_to.usage == 'internal': + # Do manual search for quant to avoid full table scan (order by id) + self._cr.execute(""" + SELECT 0 FROM stock_quant, stock_location WHERE product_id = %s AND stock_location.id = stock_quant.location_id AND + ((stock_location.parent_left >= %s AND stock_location.parent_left < %s) OR stock_location.id = %s) AND qty < 0.0 LIMIT 1 + """, (move.product_id.id, location_to.parent_left, location_to.parent_right, location_to.id)) + if self._cr.fetchone(): + quants_reconcile_sudo._quant_reconcile_negative(move) + + # In case of serial tracking, check if the product does not exist somewhere internally already + # Checking that a positive quant already exists in an internal location is too restrictive. + # Indeed, if a warehouse is configured with several steps (e.g. "Pick + Pack + Ship") and + # one step is forced (creates a quant of qty = -1.0), it is not possible afterwards to + # correct the inventory unless the product leaves the stock. + picking_type = move.picking_id and move.picking_id.picking_type_id or False + if check_lot and lot_id and move.product_id.tracking == 'serial' and ( + not picking_type or (picking_type.use_create_lots or picking_type.use_existing_lots)): + other_quants = self.search([('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id), + ('qty', '>', 0.0), ('location_id.usage', '=', 'internal')]) + if other_quants: + # We raise an error if: + # - the total quantity is strictly larger than 1.0 + # - there are more than one negative quant, to avoid situations where the user would + # force the quantity at several steps of the process + if sum(other_quants.mapped('qty')) > 1.0 or len([q for q in other_quants.mapped('qty') if q < 0]) > 1: + lot_name = self.env['stock.production.lot'].browse(lot_id).name + raise UserError(_('The serial number %s is already in stock.') % lot_name + _( + "Otherwise make sure the right stock/owner is set.")) + + @api.multi + def _quant_update_from_move(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False): + super(Quant, self)._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=False, + entire_pack=False) + if move.picking_id.force_date: + self.write({'in_date': move.picking_id.force_date}) + + def _create_account_move_line(self, move, credit_account_id, debit_account_id, journal_id): + # group quants by cost + quant_cost_qty = defaultdict(lambda: 0.0) + for quant in self: + quant_cost_qty[quant.cost] += quant.qty + AccountMove = self.env['account.move'] + for cost, qty in quant_cost_qty.iteritems(): + move_lines = move._prepare_account_move_line(qty, cost, credit_account_id, debit_account_id) + if move_lines: + if move.picking_id.force_date: + date = datetime.strptime(move.picking_id.force_date, '%Y-%m-%d %H:%M:%S') + else: + date = self._context.get('force_period_date', fields.Date.context_today(self)) + new_account_move = AccountMove.create({ + 'journal_id': journal_id, + 'line_ids': move_lines, + 'date': date, + 'ref': move.picking_id.name}) + new_account_move.post() + + +class StockMove(models.Model): + _inherit = "stock.move" + + @api.multi + def action_done(self): + """ Process completely the moves given and if all moves are done, it will finish the picking. """ + self.filtered(lambda move: move.state == 'draft').action_confirm() + + Uom = self.env['product.uom'] + Quant = self.env['stock.quant'] + + pickings = self.env['stock.picking'] + procurements = self.env['procurement.order'] + operations = self.env['stock.pack.operation'] + + remaining_move_qty = {} + + for move in self: + if move.picking_id: + pickings |= move.picking_id + remaining_move_qty[move.id] = move.product_qty + for link in move.linked_move_operation_ids: + operations |= link.operation_id + pickings |= link.operation_id.picking_id + + # Sort operations according to entire packages first, then package + lot, package only, lot only + operations = operations.sorted(key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0)) + + for operation in operations: + + # product given: result put immediately in the result package (if False: without package) + # but if pack moved entirely, quants should not be written anything for the destination package + quant_dest_package_id = operation.product_id and operation.result_package_id.id or False + entire_pack = not operation.product_id and True or False + + # compute quantities for each lot + check quantities match + lot_quantities = dict((pack_lot.lot_id.id, operation.product_uom_id._compute_quantity(pack_lot.qty, operation.product_id.uom_id) + ) for pack_lot in operation.pack_lot_ids) + + qty = operation.product_qty + if operation.product_uom_id and operation.product_uom_id != operation.product_id.uom_id: + qty = operation.product_uom_id._compute_quantity(qty, operation.product_id.uom_id) + if operation.pack_lot_ids and float_compare(sum(lot_quantities.values()), qty, precision_rounding=operation.product_id.uom_id.rounding) != 0.0: + raise UserError(_('You have a difference between the quantity on the operation and the quantities specified for the lots. ')) + + quants_taken = [] + false_quants = [] + lot_move_qty = {} + + prout_move_qty = {} + for link in operation.linked_move_operation_ids: + prout_move_qty[link.move_id] = prout_move_qty.get(link.move_id, 0.0) + link.qty + + # Process every move only once for every pack operation + for move in prout_move_qty.keys(): + # TDE FIXME: do in batch ? + move.check_tracking(operation) + + # TDE FIXME: I bet the message error is wrong + if not remaining_move_qty.get(move.id): + raise UserError(_("The roundings of your unit of measure %s on the move vs. %s on the product don't allow to do these operations or you are not transferring the picking at once. ") % (move.product_uom.name, move.product_id.uom_id.name)) + + if not operation.pack_lot_ids: + preferred_domain_list = [[('reservation_id', '=', move.id)], [('reservation_id', '=', False)], ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]] + quants = Quant.quants_get_preferred_domain( + prout_move_qty[move], move, ops=operation, domain=[('qty', '>', 0)], + preferred_domain_list=preferred_domain_list) + Quant.quants_move(quants, move, operation.location_dest_id, location_from=operation.location_id, + lot_id=False, owner_id=operation.owner_id.id, src_package_id=operation.package_id.id, + dest_package_id=quant_dest_package_id, entire_pack=entire_pack) + else: + # Check what you can do with reserved quants already + qty_on_link = prout_move_qty[move] + rounding = operation.product_id.uom_id.rounding + for reserved_quant in move.reserved_quant_ids: + if (reserved_quant.owner_id.id != operation.owner_id.id) or (reserved_quant.location_id.id != operation.location_id.id) or \ + (reserved_quant.package_id.id != operation.package_id.id): + continue + if not reserved_quant.lot_id: + false_quants += [reserved_quant] + elif float_compare(lot_quantities.get(reserved_quant.lot_id.id, 0), 0, precision_rounding=rounding) > 0: + if float_compare(lot_quantities[reserved_quant.lot_id.id], reserved_quant.qty, precision_rounding=rounding) >= 0: + lot_quantities[reserved_quant.lot_id.id] -= reserved_quant.qty + quants_taken += [(reserved_quant, reserved_quant.qty)] + qty_on_link -= reserved_quant.qty + else: + quants_taken += [(reserved_quant, lot_quantities[reserved_quant.lot_id.id])] + lot_quantities[reserved_quant.lot_id.id] = 0 + qty_on_link -= lot_quantities[reserved_quant.lot_id.id] + lot_move_qty[move.id] = qty_on_link + + remaining_move_qty[move.id] -= prout_move_qty[move] + + # Handle lots separately + if operation.pack_lot_ids: + # TDE FIXME: fix call to move_quants_by_lot to ease understanding + self._move_quants_by_lot(operation, lot_quantities, quants_taken, false_quants, lot_move_qty, quant_dest_package_id) + + # Handle pack in pack + if not operation.product_id and operation.package_id and operation.result_package_id.id != operation.package_id.parent_id.id: + operation.package_id.sudo().write({'parent_id': operation.result_package_id.id}) + + # Check for remaining qtys and unreserve/check move_dest_id in + move_dest_ids = set() + for move in self: + if float_compare(remaining_move_qty[move.id], 0, precision_rounding=move.product_id.uom_id.rounding) > 0: # In case no pack operations in picking + move.check_tracking(False) # TDE: do in batch ? redone ? check this + + preferred_domain_list = [[('reservation_id', '=', move.id)], [('reservation_id', '=', False)], ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]] + quants = Quant.quants_get_preferred_domain( + remaining_move_qty[move.id], move, domain=[('qty', '>', 0)], + preferred_domain_list=preferred_domain_list) + Quant.quants_move( + quants, move, move.location_dest_id, + lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id) + + # If the move has a destination, add it to the list to reserve + if move.move_dest_id and move.move_dest_id.state in ('waiting', 'confirmed'): + move_dest_ids.add(move.move_dest_id.id) + + if move.procurement_id: + procurements |= move.procurement_id + + # unreserve the quants and make them available for other operations/moves + move.quants_unreserve() + + # Check the packages have been placed in the correct locations + self.mapped('quant_ids').filtered(lambda quant: quant.package_id and quant.qty > 0).mapped('package_id')._check_location_constraint() + + # set the move as done + # setting force_date into stock moves + f_date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + for move in self: + if move.picking_id.force_date: + f_date = move.picking_id.force_date + self.write({'state': 'done', 'date': f_date}) + # self.write({'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) + procurements.check() + # assign destination moves + if move_dest_ids: + # TDE FIXME: record setise me + self.browse(list(move_dest_ids)).action_assign() + + pickings.filtered(lambda picking: picking.state == 'done' and not picking.date_done).write( + {'date_done': f_date}) + # pickings.filtered(lambda picking: picking.state == 'done' and not picking.date_done).write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) + + return True diff --git a/stock_force_date/static/description/banner.jpg b/stock_force_date/static/description/banner.jpg new file mode 100644 index 000000000..6f84c7b2b Binary files /dev/null and b/stock_force_date/static/description/banner.jpg differ diff --git a/stock_force_date/static/description/cybro_logo.png b/stock_force_date/static/description/cybro_logo.png new file mode 100644 index 000000000..bb309114c Binary files /dev/null and b/stock_force_date/static/description/cybro_logo.png differ diff --git a/stock_force_date/static/description/history_default.png b/stock_force_date/static/description/history_default.png new file mode 100644 index 000000000..9ab78e18a Binary files /dev/null and b/stock_force_date/static/description/history_default.png differ diff --git a/stock_force_date/static/description/icon.png b/stock_force_date/static/description/icon.png new file mode 100644 index 000000000..019d661e4 Binary files /dev/null and b/stock_force_date/static/description/icon.png differ diff --git a/stock_force_date/static/description/index.html b/stock_force_date/static/description/index.html new file mode 100644 index 000000000..604dff38f --- /dev/null +++ b/stock_force_date/static/description/index.html @@ -0,0 +1,156 @@ +
+
+

Stock Force Date

+

Cybrosys Technologies

+
+
+ +
+
+

Force a date on stock picking

+

+ Help you to give a date on Stock picking, So that you can force Odoo to use the same date on all its related records like, stock move, quants and stock Journal entry. +

+
+ +
+

By Default in Odoo,

+
+ +
+
+
+

+

We cannot record a stock transfer happened in a past date with its correct effect.

+

+
+
+
+ +
+
+
+
+ +
+
+
+

+

Even after we change the scheduled date to a past date wont do the job.

+

+
+
+
+ +
+
+
+
+ +
+
+
+

+

Stock move is recorded with the current date

+

+
+
+
+ +
+
+
+
+ +
+
+
+

+

Stock history is also recorded with the current date

+

+
+
+
+ +
+
+
+
+ +
+

Using this module,

+
+ +
+
+
+

+

User can give a specific date as 'Force Date' in Stock Picking

+

+
+
+
+ +
+
+
+
+ +
+
+
+

+

That 'Force Date' will be the date of related stock moves

+

+
+
+
+ +
+
+
+
+ +
+
+
+

+

Stock history is also recorded with that 'Force Date'

+

+
+
+
+ +
+
+
+
+ +
+ +
+

Need Any Help?

+ +
+ diff --git a/stock_force_date/static/description/picking.png b/stock_force_date/static/description/picking.png new file mode 100644 index 000000000..e35220b1c Binary files /dev/null and b/stock_force_date/static/description/picking.png differ diff --git a/stock_force_date/static/description/picking_default.png b/stock_force_date/static/description/picking_default.png new file mode 100644 index 000000000..784d3e365 Binary files /dev/null and b/stock_force_date/static/description/picking_default.png differ diff --git a/stock_force_date/static/description/picking_default_2.png b/stock_force_date/static/description/picking_default_2.png new file mode 100644 index 000000000..243efb574 Binary files /dev/null and b/stock_force_date/static/description/picking_default_2.png differ diff --git a/stock_force_date/static/description/stock_history.png b/stock_force_date/static/description/stock_history.png new file mode 100644 index 000000000..7c7db963c Binary files /dev/null and b/stock_force_date/static/description/stock_history.png differ diff --git a/stock_force_date/static/description/stock_move.png b/stock_force_date/static/description/stock_move.png new file mode 100644 index 000000000..73b0b46d6 Binary files /dev/null and b/stock_force_date/static/description/stock_move.png differ diff --git a/stock_force_date/static/description/stock_move_default.png b/stock_force_date/static/description/stock_move_default.png new file mode 100644 index 000000000..74fd500fc Binary files /dev/null and b/stock_force_date/static/description/stock_move_default.png differ diff --git a/stock_force_date/views/stock_view.xml b/stock_force_date/views/stock_view.xml new file mode 100644 index 000000000..3cd7e3c7e --- /dev/null +++ b/stock_force_date/views/stock_view.xml @@ -0,0 +1,15 @@ + + + + + az.stock.picking.form + stock.picking + + + + + + + + + \ No newline at end of file