Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion purchase_sale_stock_inter_company/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"installable": True,
"auto_install": True,
"depends": ["purchase_sale_inter_company", "sale_stock", "purchase_stock"],
"data": ["views/res_config_view.xml"],
"data": ["views/res_config_view.xml", "views/stock_picking_type_views.xml"],
}
2 changes: 2 additions & 0 deletions purchase_sale_stock_inter_company/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
from . import res_company
from . import res_config
from . import stock_picking
from . import stock_production_lot
from . import stock_picking_type
190 changes: 161 additions & 29 deletions purchase_sale_stock_inter_company/models/stock_picking.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,128 @@
# Copyright 2018 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import _, fields, models
from odoo import SUPERUSER_ID, _, fields, models
from odoo.exceptions import UserError


class StockPicking(models.Model):
_inherit = "stock.picking"

intercompany_picking_id = fields.Many2one(comodel_name="stock.picking")
intercompany_create_lots_mode = fields.Selection(
related="picking_type_id.intercompany_create_lots_mode"
)

def _get_product_intercompany_qty_done_dict(self, sale_move_lines, po_move_lines):
product = po_move_lines[0].product_id
qty_done = sum(sale_move_lines.mapped("qty_done"))
res = {product: qty_done}
return res

def _get_intercompany_move_lots(self, sale_move_lines, po_moves_open, **kwargs):
po_move_lots = self.env["stock.lot"]
lot_creation_mode = self.intercompany_create_lots_mode
if lot_creation_mode == "same":
for sale_lot in sale_move_lines.lot_id:
po_move_lots |= sale_lot.get_inter_company_lot(
po_moves_open.company_id, **kwargs
)
elif lot_creation_mode == "manual":
po_move_lots |= po_moves_open.mapped("lot_ids")
return po_move_lots

def _check_manual_lots(self, move, product):
self.ensure_one()
lot_names = move.move_line_ids.mapped("lot_name")
lot_ids = move.lot_ids
if (
self.intercompany_create_lots_mode == "manual"
and product.tracking != "none"
and move.quantity_done
and not lot_names
and not lot_ids
):
raise UserError(
_(
"To validate the delivery, you must first assign lot/serial numbers"
" manually on the receipt of the intercompany purchase."
)
)

def _set_intercompany_picking_qty(self, purchase):
po_picks = self.browse()
sale_line_ids = self.move_line_ids.mapped("move_id.sale_line_id")
sale_line_ids = self.move_line_ids.move_id.sale_line_id
for sale_line in sale_line_ids:
sale_move_lines = self.move_line_ids.filtered(
lambda ml: ml.move_id.sale_line_id == sale_line
)
po_move_lines = sale_line.auto_purchase_line_id.move_ids.mapped(
"move_line_ids"
po_moves_open = sale_line.auto_purchase_line_id.move_ids.filtered(
lambda sm: sm.state not in ["draft", "done", "cancel"]
)
if not po_move_lines:
raise UserError(
_(
"There's no corresponding line in PO %(po)s for assigning "
"qty from %(pick_name)s for product %(product)s"
)
% (
{
"po": purchase.name,
"pick_name": self.name,
"product": sale_line.product_id.name,
}
)
if not po_moves_open:
po_moves_done = sale_line.auto_purchase_line_id.move_ids.filtered(
lambda sm: sm.state == "done"
)
if po_moves_done:
for done_move in po_moves_done:
new_move = done_move.copy(
{
"quantity_done": 0.0,
"move_line_ids": False,
}
)
po_moves_open |= new_move
else:
note = _(
"Mismatch between move lines with the "
"corresponding PO %(po)s for assigning "
"quantities and lots from %(pick_name)s for product %(product)s"
) % {
"po": purchase.name,
"pick_name": self.name,
"product": sale_line.product_id.name,
}
purchase.activity_schedule(
"mail.mail_activity_data_warning",
date_deadline=fields.Date.today(),
note=note,
user_id=(
self.sale_id.user_id.id
or self.sale_id.team_id.user_id.id
or SUPERUSER_ID
),
)
continue
po_moves_open.picking_id.action_assign()
product_qty_done = self._get_product_intercompany_qty_done_dict(
sale_move_lines, po_move_lines
sale_move_lines, po_moves_open.move_line_ids
)
po_move_lots = self._get_intercompany_move_lots(
sale_move_lines, po_moves_open
)
for product, qty_done in product_qty_done.items():
product_po_mls = po_move_lines.filtered(
product_po_moves = po_moves_open.filtered(
lambda x: x.exists() and x.product_id == product
)
product_po_lots = po_move_lots.filtered(
lambda x: x.product_id == product
)
for po_move_line in product_po_mls:
if po_move_line.reserved_qty >= qty_done:
po_move_line.qty_done = qty_done
for po_move in product_po_moves:
if po_move.product_uom_qty >= qty_done:
po_move.quantity_done = qty_done
po_move.lot_ids = product_po_lots
qty_done = 0.0
elif po_move_line.reserved_qty:
po_move_line.qty_done = po_move_line.reserved_qty
qty_done -= po_move_line.reserved_qty
po_picks |= po_move_line.picking_id
if qty_done and product_po_mls:
product_po_mls[-1:].qty_done += qty_done
else:
po_move.quantity_done = po_move.product_uom_qty
po_move.lot_ids = product_po_lots[: po_move.product_uom_qty]
product_po_lots = product_po_lots[po_move.product_uom_qty :]
qty_done -= po_move.product_uom_qty
self._check_manual_lots(po_move, product)
po_picks |= po_move.picking_id
if qty_done and product_po_moves:
product_po_moves[-1:].quantity_done += qty_done
product_po_moves[-1:].lot_ids |= product_po_lots
self._check_manual_lots(product_po_moves[-1:], product)
return po_picks

def _action_done(self):
Expand All @@ -71,7 +137,73 @@ def _action_done(self):
continue
purchase.picking_ids.write({"intercompany_picking_id": pick.id})
po_picks |= pick._set_intercompany_picking_qty(purchase)
# Transfer dropship pickings

# Process Returns (incoming from customer)
for pick in self.filtered(
lambda x: x.location_id.usage == "customer" and x.origin
).sudo():
source_picking = pick.move_ids.origin_returned_move_id.picking_id
if not source_picking:
continue
sale_order = source_picking.sale_id
if not sale_order or not sale_order.auto_purchase_order_id:
continue
purchase = sale_order.auto_purchase_order_id
po_delivery = purchase.picking_ids.filtered(
lambda p: p.intercompany_picking_id == source_picking
and p.state == "done"
)
if not po_delivery:
continue
po_return = po_delivery.move_ids.returned_move_ids.picking_id.filtered(
lambda p: p.state not in ["done", "cancel"]
)
if not po_return:
po_return = pick._create_intercompany_return(po_delivery[-1], purchase)
if po_return:
po_picks |= pick._sync_return_quantities(po_return, purchase)

# Transfer dropship/return pickings
for po_pick in po_picks.sudo():
po_pick.with_company(po_pick.company_id.id)._action_done()
return super()._action_done()

def _create_intercompany_return(self, po_delivery, purchase):
return_wizard = (
self.env["stock.return.picking"]
.with_context(active_id=po_delivery.id, active_model="stock.picking")
.with_company(purchase.company_id.id)
.create({})
)
return_wizard._onchange_picking_id()
# TODO: filter lines by product/qty when the return is not complete
result = return_wizard.create_returns()
po_return = self.env["stock.picking"].browse(result["res_id"])
return po_return

def _sync_return_quantities(self, po_return, purchase):
po_return.action_assign()
for return_move_line in self.move_line_ids:
# TODO: filter lines by product/qty when the return is not complete
po_move_lines = po_return.move_line_ids
qty_to_set = return_move_line.qty_done
for po_ml in po_move_lines:
if qty_to_set <= 0:
break
available_qty = po_ml.reserved_uom_qty or po_ml.product_uom_qty
qty_done = min(qty_to_set, available_qty)
po_ml.qty_done = qty_done
if return_move_line.lot_id:
matching_lot = self.env["stock.lot"].search(
[
("name", "=", return_move_line.lot_id.name),
("product_id", "=", po_ml.product_id.id),
("company_id", "=", purchase.company_id.id),
],
limit=1,
)
if matching_lot:
po_ml.lot_id = matching_lot
qty_to_set -= qty_done
po_return.write({"intercompany_picking_id": self.id})
return po_return
20 changes: 20 additions & 0 deletions purchase_sale_stock_inter_company/models/stock_picking_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import fields, models


class StockPickingType(models.Model):
_inherit = "stock.picking.type"

intercompany_create_lots_mode = fields.Selection(
[("same", "Use Same Number"), ("manual", "Assign Number Manually")],
string="Lots Creation Mode in Intercompany",
default="same",
required=True,
help=" Select which logic to follow when a receipt with products "
"tracked by Lots/Serial numbers is auto validated by an intercompany flow:\n"
"* Use Same Number: Create a Lot/Serial number with the same number as in the "
"originating company.\n"
"* Assign Number Manually: Assign manually in the receipt the lot number to be used.\n",
)
44 changes: 44 additions & 0 deletions purchase_sale_stock_inter_company/models/stock_production_lot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2023 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# @author Guillaume MASSON <guillaume.masson@groupevoltaire.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models


class StockProductionLot(models.Model):
_inherit = "stock.lot"

def _get_inter_company_lot_product(self, **kwargs):
"""Return the product to use when searching/creating the lot.
Hook for customization. By default, it’s just the selling company
lot’s product.
"""
self.ensure_one()
return self.product_id

def prepare_inter_company_lot_values(self, company, product):
res = {
"name": self.name,
"product_id": product.id,
"company_id": company.id,
}
if "expiration_date" in self._fields:
res.update(expiration_date=self.expiration_date)
return res

def get_inter_company_lot(self, company, **kwargs):
self.ensure_one()
product = self._get_inter_company_lot_product(**kwargs)
lot = self.sudo().search(
[
("name", "=", self.name),
("product_id", "=", product.id),
("company_id", "=", company.id),
]
)
if not lot:
lot = self.sudo().create(
self.prepare_inter_company_lot_values(company, product)
)
return lot
Loading