534 lines
19 KiB
Python
534 lines
19 KiB
Python
# Copyright (C) 2019-Today: Odoo Community Association (OCA)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
|
|
JOURNAL_TYPE_MAP = {
|
|
("outgoing", "customer"): ["sale"],
|
|
("outgoing", "supplier"): ["purchase"],
|
|
("outgoing", "transit"): ["sale", "purchase"],
|
|
("incoming", "supplier"): ["purchase"],
|
|
("incoming", "customer"): ["sale"],
|
|
("incoming", "transit"): ["purchase", "sale"],
|
|
}
|
|
|
|
INVOICE_TYPE_MAP = {
|
|
# Picking Type Code | Local Origin Usage | Local Dest Usage
|
|
("outgoing", "internal", "customer"): "out_invoice",
|
|
("incoming", "customer", "internal"): "out_refund",
|
|
("incoming", "customer", "customer"): "out_refund",
|
|
("incoming", "supplier", "internal"): "in_invoice",
|
|
("incoming", "supplier", "customer"): "in_invoice",
|
|
("outgoing", "internal", "supplier"): "in_refund",
|
|
("outgoing", "internal", "internal"): "in_refund",
|
|
("incoming", "transit", "internal"): "in_invoice",
|
|
("outgoing", "transit", "supplier"): "in_refund",
|
|
("outgoing", "transit", "customer"): "out_invoice",
|
|
}
|
|
|
|
|
|
class StockInvoiceOnshipping(models.TransientModel):
|
|
_name = "stock.invoice.onshipping"
|
|
_description = "Stock Invoice Onshipping"
|
|
|
|
@api.model
|
|
def _get_journal_type(self):
|
|
active_ids = self.env.context.get("active_ids", [])
|
|
if active_ids:
|
|
active_ids = active_ids[0]
|
|
pick_obj = self.env["stock.picking"]
|
|
picking = pick_obj.browse(active_ids)
|
|
if not picking or not picking.move_ids:
|
|
return "sale"
|
|
pick_type_code = picking.picking_type_id.code
|
|
line = fields.first(picking.move_ids)
|
|
if pick_type_code == "incoming":
|
|
usage = line.location_id.usage
|
|
else:
|
|
usage = line.location_dest_id.usage
|
|
return JOURNAL_TYPE_MAP.get((pick_type_code, usage), ["sale"])[0]
|
|
|
|
journal_type = fields.Selection(
|
|
selection=[
|
|
("purchase", "Create Supplier Invoice"),
|
|
("sale", "Create Customer Invoice"),
|
|
],
|
|
default=_get_journal_type,
|
|
readonly=True,
|
|
)
|
|
group = fields.Selection(
|
|
selection=[
|
|
("picking", "Picking"),
|
|
("partner", "Partner"),
|
|
("partner_product", "Partner/Product"),
|
|
],
|
|
default="picking",
|
|
help="Group pickings/moves to create invoice(s):\n"
|
|
"Picking: One invoice per picking;\n"
|
|
"Partner: One invoice for each picking's partner;\n"
|
|
"Partner/Product: One invoice per picking's partner and group "
|
|
"product into a single invoice line.",
|
|
required=True,
|
|
)
|
|
invoice_date = fields.Date()
|
|
sale_journal = fields.Many2one(
|
|
comodel_name="account.journal",
|
|
domain="[('type', '=', 'sale')]",
|
|
default=lambda self: self._default_journal("sale"),
|
|
ondelete="cascade",
|
|
)
|
|
purchase_journal = fields.Many2one(
|
|
comodel_name="account.journal",
|
|
domain="[('type', '=', 'purchase')]",
|
|
default=lambda self: self._default_journal("purchase"),
|
|
ondelete="cascade",
|
|
)
|
|
show_sale_journal = fields.Boolean()
|
|
show_purchase_journal = fields.Boolean()
|
|
|
|
@api.model
|
|
def default_get(self, fields_list):
|
|
"""
|
|
Inherit to add default invoice_date
|
|
:param fields_list: list of str
|
|
:return: dict
|
|
"""
|
|
result = super().default_get(fields_list)
|
|
result.update({"invoice_date": fields.Date.today()})
|
|
return result
|
|
|
|
@api.onchange("group")
|
|
def onchange_group(self):
|
|
self.ensure_one()
|
|
(
|
|
sale_pickings,
|
|
sale_refund_pickings,
|
|
purchase_pickings,
|
|
purchase_refund_pickings,
|
|
) = self.get_split_pickings()
|
|
self.show_sale_journal = bool(sale_pickings)
|
|
self.show_purchase_journal = bool(purchase_pickings)
|
|
|
|
def get_partner_sum(self, pickings, partner, inv_type, picking_type, usage):
|
|
pickings = pickings.filtered(
|
|
lambda x: x.picking_type_id.code == picking_type and x.partner_id == partner
|
|
)
|
|
lines = pickings.mapped("move_ids")
|
|
if picking_type == "outgoing":
|
|
moves = lines.filtered(lambda x: x.location_dest_id.usage == usage)
|
|
else:
|
|
moves = lines.filtered(lambda x: x.location_id.usage == usage)
|
|
total = sum(
|
|
(
|
|
m._get_price_unit_invoice(inv_type, m.picking_id.partner_id)
|
|
* m.product_uom_qty
|
|
)
|
|
for m in moves
|
|
)
|
|
return total, moves.mapped("picking_id")
|
|
|
|
def get_split_pickings(self):
|
|
self.ensure_one()
|
|
picking_obj = self.env["stock.picking"]
|
|
pickings = picking_obj.browse(self.env.context.get("active_ids", []))
|
|
if self.group != "picking":
|
|
return self.get_split_pickings_grouped(pickings)
|
|
return self.get_split_pickings_nogrouped(pickings)
|
|
|
|
def get_split_pickings_grouped(self, pickings):
|
|
sale_pickings = self.env["stock.picking"].browse()
|
|
sale_refund_pickings = self.env["stock.picking"].browse()
|
|
purchase_pickings = self.env["stock.picking"].browse()
|
|
purchase_refund_pickings = self.env["stock.picking"].browse()
|
|
|
|
for partner in pickings.mapped("partner_id"):
|
|
so_sum, so_pickings = self.get_partner_sum(
|
|
pickings, partner, "out_invoice", "outgoing", "customer"
|
|
)
|
|
si_sum, si_pickings = self.get_partner_sum(
|
|
pickings, partner, "out_invoice", "incoming", "customer"
|
|
)
|
|
if (so_sum - si_sum) >= 0:
|
|
sale_pickings |= so_pickings | si_pickings
|
|
else:
|
|
sale_refund_pickings |= so_pickings | si_pickings
|
|
pi_sum, pi_pickings = self.get_partner_sum(
|
|
pickings, partner, "in_invoice", "incoming", "supplier"
|
|
)
|
|
po_sum, po_pickings = self.get_partner_sum(
|
|
pickings, partner, "in_invoice", "outgoing", "supplier"
|
|
)
|
|
if (pi_sum - po_sum) >= 0:
|
|
purchase_pickings |= pi_pickings | po_pickings
|
|
else:
|
|
purchase_refund_pickings |= pi_pickings | po_pickings
|
|
|
|
return (
|
|
sale_pickings,
|
|
sale_refund_pickings,
|
|
purchase_pickings,
|
|
purchase_refund_pickings,
|
|
)
|
|
|
|
def get_split_pickings_nogrouped(self, pickings):
|
|
first = fields.first
|
|
sale_pickings = pickings.filtered(
|
|
lambda x: x.picking_type_id.code == "outgoing"
|
|
and first(x.move_ids).location_dest_id.usage == "customer"
|
|
)
|
|
sale_refund_pickings = pickings.filtered(
|
|
lambda x: x.picking_type_id.code == "incoming"
|
|
and first(x.move_ids).location_id.usage == "customer"
|
|
)
|
|
purchase_pickings = pickings.filtered(
|
|
lambda x: x.picking_type_id.code == "incoming"
|
|
and first(x.move_ids).location_id.usage == "supplier"
|
|
)
|
|
purchase_refund_pickings = pickings.filtered(
|
|
lambda x: x.picking_type_id.code == "outgoing"
|
|
and first(x.move_ids).location_dest_id.usage == "supplier"
|
|
)
|
|
|
|
return (
|
|
sale_pickings,
|
|
sale_refund_pickings,
|
|
purchase_pickings,
|
|
purchase_refund_pickings,
|
|
)
|
|
|
|
@api.model
|
|
def _default_journal(self, journal_type):
|
|
"""
|
|
Get the default journal based on the given type
|
|
:param journal_type: str
|
|
:return: account.journal recordset
|
|
"""
|
|
default_journal = self.env["account.journal"].search(
|
|
[
|
|
("type", "=", journal_type),
|
|
("company_id", "=", self.env.company.id),
|
|
],
|
|
limit=1,
|
|
)
|
|
return default_journal
|
|
|
|
def action_generate(self):
|
|
"""
|
|
Launch the invoice generation
|
|
:return:
|
|
"""
|
|
self.ensure_one()
|
|
invoices = self._action_generate_invoices()
|
|
if not invoices:
|
|
raise UserError(_("No invoice created!"))
|
|
|
|
# Update the state on pickings related to new invoices only
|
|
self._update_picking_invoice_status(invoices.mapped("picking_ids"))
|
|
|
|
inv_type = self._get_invoice_type()
|
|
if inv_type in ["out_invoice", "out_refund"]:
|
|
xmlid = "account.action_move_out_invoice_type"
|
|
else:
|
|
xmlid = "account.action_move_in_invoice_type"
|
|
action_dict = self.env["ir.actions.act_window"]._for_xml_id(xmlid)
|
|
|
|
if len(invoices) > 1:
|
|
action_dict["domain"] = [("id", "in", invoices.ids)]
|
|
elif len(invoices) == 1:
|
|
if inv_type in ["out_invoice", "out_refund"]:
|
|
form_view = [(self.env.ref("account.view_move_form").id, "form")]
|
|
else:
|
|
form_view = [(self.env.ref("account.view_move_form").id, "form")]
|
|
if "views" in action_dict:
|
|
action_dict["views"] = form_view + [
|
|
(state, view)
|
|
for state, view in action_dict["views"]
|
|
if view != "form"
|
|
]
|
|
else:
|
|
action_dict["views"] = form_view
|
|
action_dict["res_id"] = invoices.ids[0]
|
|
|
|
return action_dict
|
|
|
|
def _load_pickings(self):
|
|
"""
|
|
Load pickings from context
|
|
:return: stock.picking recordset
|
|
"""
|
|
picking_obj = self.env["stock.picking"]
|
|
active_ids = self.env.context.get("active_ids", [])
|
|
pickings = picking_obj.browse(active_ids)
|
|
pickings = pickings.filtered(lambda p: p.invoice_state == "2binvoiced")
|
|
return pickings
|
|
|
|
def _get_journal(self):
|
|
"""
|
|
Get the journal depending on the journal_type
|
|
:return: account.journal recordset
|
|
"""
|
|
self.ensure_one()
|
|
journal_field = f"{self.journal_type}_journal"
|
|
journal = self[journal_field]
|
|
return journal
|
|
|
|
def _get_invoice_type(self):
|
|
"""
|
|
Get the invoice type
|
|
:return: str
|
|
"""
|
|
self.ensure_one()
|
|
|
|
active_ids = self.env.context.get("active_ids", [])
|
|
if active_ids:
|
|
active_ids = active_ids[0]
|
|
picking = self.env["stock.picking"].browse(active_ids)
|
|
|
|
inv_type = (
|
|
INVOICE_TYPE_MAP.get(
|
|
(
|
|
picking.picking_type_code,
|
|
picking.location_id.usage,
|
|
picking.location_dest_id.usage,
|
|
)
|
|
)
|
|
or "out_invoice"
|
|
)
|
|
|
|
return inv_type
|
|
|
|
@api.model
|
|
def _get_picking_key(self, picking):
|
|
"""
|
|
Get the key for the given picking.
|
|
By default, it's based on the invoice partner and the picking_type_id
|
|
of the picking
|
|
:param picking: stock.picking recordset
|
|
:return: key (tuple,...)
|
|
"""
|
|
key = (picking,)
|
|
if self.group in ["partner", "partner_product"]:
|
|
# Pickings with same Partner to create Invoice but the
|
|
# Partner to Shipping is different should not be grouping.
|
|
key = (
|
|
picking._get_partner_to_invoice(),
|
|
picking.picking_type_id,
|
|
picking.partner_id,
|
|
)
|
|
return key
|
|
|
|
def _group_pickings(self, pickings):
|
|
"""
|
|
Group given picking
|
|
:param pickings:
|
|
:return: list of stock.picking recordset
|
|
"""
|
|
grouped_picking = {}
|
|
pickings = pickings.filtered(lambda p: p.invoice_state == "2binvoiced")
|
|
for picking in pickings:
|
|
key = self._get_picking_key(picking)
|
|
picks_grouped = grouped_picking.get(key, self.env["stock.picking"].browse())
|
|
picks_grouped |= picking
|
|
grouped_picking.update({key: picks_grouped})
|
|
return grouped_picking.values()
|
|
|
|
def _simulate_invoice_onchange(self, values):
|
|
"""
|
|
Simulate onchange for invoice
|
|
:param values: dict
|
|
:return: dict
|
|
"""
|
|
invoice = self.env["account.move"].new(values.copy())
|
|
invoice._onchange_partner_id()
|
|
invoice._inverse_company_id()
|
|
invoice._inverse_currency_id()
|
|
invoice._onchange_date()
|
|
new_values = invoice._convert_to_write(invoice._cache)
|
|
# Ensure basic values are not updated
|
|
values.update(new_values)
|
|
return invoice, values
|
|
|
|
def _build_invoice_values_from_pickings(self, pickings):
|
|
"""
|
|
Build dict to create a new invoice from given pickings
|
|
:param pickings: stock.picking recordset
|
|
:return: dict
|
|
"""
|
|
picking = fields.first(pickings)
|
|
partner_id = picking._get_partner_to_invoice()
|
|
partner = self.env["res.partner"].browse(partner_id)
|
|
inv_type = self._get_invoice_type()
|
|
currency = self.env.company.currency_id
|
|
if partner:
|
|
code = picking.picking_type_id.code
|
|
if partner.property_product_pricelist and code == "outgoing":
|
|
currency = partner.property_product_pricelist.currency_id
|
|
journal = self._get_journal()
|
|
invoice_obj = self.env["account.move"]
|
|
values = invoice_obj.default_get(invoice_obj.fields_get().keys())
|
|
values.update(
|
|
{
|
|
"invoice_origin": ", ".join(pickings.mapped("name")),
|
|
"partner_id": partner_id,
|
|
"move_type": inv_type,
|
|
"currency_id": currency.id,
|
|
"journal_id": journal.id,
|
|
}
|
|
)
|
|
|
|
invoice, values = self._simulate_invoice_onchange(values)
|
|
return invoice, values
|
|
|
|
def _get_move_key(self, move):
|
|
"""
|
|
Get the key based on the given move
|
|
:param move: stock.move recordset
|
|
:return: (key,)
|
|
"""
|
|
key = (move,)
|
|
if self.group == "partner_product":
|
|
key = (move.product_id,)
|
|
return key
|
|
|
|
def _group_moves(self, moves):
|
|
"""
|
|
Possibility to group moves (to create 1 invoice line with many moves)
|
|
:param moves: stock.move recordset
|
|
:return: list of stock.move recordset
|
|
"""
|
|
grouped_moves = {}
|
|
moves = moves.filtered(lambda m: m.invoice_state == "2binvoiced")
|
|
for move in moves:
|
|
key = self._get_move_key(move)
|
|
move_grouped = grouped_moves.get(key, self.env["stock.move"].browse())
|
|
move_grouped |= move
|
|
grouped_moves.update({key: move_grouped})
|
|
return grouped_moves.values()
|
|
|
|
def _simulate_invoice_line_onchange(self, values, price_unit=None):
|
|
"""
|
|
Simulate onchange for invoice line
|
|
:param values: dict
|
|
:return: dict
|
|
"""
|
|
line = self.env["account.move.line"].new(values.copy())
|
|
line._inverse_partner_id()
|
|
line._inverse_product_id()
|
|
line._inverse_account_id()
|
|
line._inverse_amount_currency()
|
|
new_values = line._convert_to_write(line._cache)
|
|
if price_unit:
|
|
new_values["price_unit"] = price_unit
|
|
# Ensure basic values are not updated
|
|
values.update(new_values)
|
|
return values
|
|
|
|
def _get_invoice_line_values(self, moves, invoice_values, invoice):
|
|
"""
|
|
Create invoice line values from given moves
|
|
:param moves: stock.move
|
|
:param invoice: account.move
|
|
:return: dict
|
|
"""
|
|
move = fields.first(moves)
|
|
product = move.product_id
|
|
partner_id = self.env["res.partner"].browse(invoice_values["partner_id"])
|
|
inv_type = invoice_values["move_type"]
|
|
quantity = 0
|
|
move_line_ids = []
|
|
for move in moves:
|
|
qty = move.product_uom_qty
|
|
loc = move.location_id
|
|
loc_dst = move.location_dest_id
|
|
# Better to understand with IF/ELIF than many OR
|
|
if inv_type == "out_invoice" and loc.usage == "customer":
|
|
qty *= -1
|
|
elif inv_type == "out_refund" and loc_dst.usage == "customer":
|
|
qty *= -1
|
|
elif inv_type == "in_invoice" and loc_dst.usage == "supplier":
|
|
qty *= -1
|
|
elif inv_type == "in_refund" and loc.usage == "supplier":
|
|
qty *= -1
|
|
quantity += qty
|
|
move_line_ids.append((4, move.id))
|
|
price = moves._get_price_unit_invoice(inv_type, partner_id, quantity)
|
|
line_obj = self.env["account.move.line"]
|
|
values = line_obj.default_get(line_obj.fields_get().keys())
|
|
values.update(
|
|
{
|
|
"name": move.name,
|
|
"product_id": product.id,
|
|
"quantity": quantity,
|
|
"price_unit": price,
|
|
"move_line_ids": move_line_ids,
|
|
"move_id": invoice.id,
|
|
}
|
|
)
|
|
values = self._simulate_invoice_line_onchange(values, price_unit=price)
|
|
return values
|
|
|
|
def _update_picking_invoice_status(self, pickings):
|
|
"""
|
|
Update invoice_state on pickings
|
|
:param pickings: stock.picking recordset
|
|
:return: stock.picking recordset
|
|
"""
|
|
return pickings.set_as_invoiced()
|
|
|
|
def ungroup_moves(self, grouped_moves_list):
|
|
"""Ungroup your moves, split them again, grouping by
|
|
fiscal position, max itens per invoice and etc
|
|
:param grouped_moves_list:
|
|
:return: list of grouped moves list
|
|
"""
|
|
return [grouped_moves_list]
|
|
|
|
def _create_invoice(self, invoice_values):
|
|
"""Override this method if you need to change any values of the
|
|
invoice and the lines before the invoice creation
|
|
:param invoice_values: dict with the invoice and its lines
|
|
:return: invoice
|
|
"""
|
|
return self.env["account.move"].create(invoice_values)
|
|
|
|
def _action_generate_invoices(self):
|
|
"""
|
|
Action to generate invoices based on pickings
|
|
:return: account.move recordset
|
|
"""
|
|
pickings = self._load_pickings()
|
|
company = pickings.mapped("company_id")
|
|
if company and company != self.env.company:
|
|
raise UserError(_("All pickings are not related to your company!"))
|
|
pick_list = self._group_pickings(pickings)
|
|
invoices = self.env["account.move"].browse()
|
|
for pickings in pick_list:
|
|
moves = pickings.mapped("move_ids")
|
|
grouped_moves_list = self._group_moves(moves)
|
|
parts = self.ungroup_moves(grouped_moves_list)
|
|
for moves_list in parts:
|
|
invoice, invoice_values = self._build_invoice_values_from_pickings(
|
|
pickings
|
|
)
|
|
lines = [(5, 0, {})]
|
|
line_values = False
|
|
for moves in moves_list:
|
|
line_values = self._get_invoice_line_values(
|
|
moves, invoice_values, invoice
|
|
)
|
|
if line_values:
|
|
lines.append((0, 0, line_values))
|
|
if line_values: # Only create the invoice if it has lines
|
|
invoice_values["invoice_line_ids"] = lines
|
|
# Necessary inform line_ids with empty list to avoid error
|
|
# addons/account/models/account_move.py", line 2251,
|
|
# in _sanitize_vals
|
|
# assert command not in (Command.SET, Command.CLEAR)
|
|
invoice_values["line_ids"] = []
|
|
invoice_values["invoice_date"] = self.invoice_date
|
|
invoice = self._create_invoice(invoice_values)
|
|
invoice._compute_amount()
|
|
invoices |= invoice
|
|
return invoices
|