353 lines
14 KiB
Python
353 lines
14 KiB
Python
# Copyright (C) 2020-TODAY KMEE
|
|
# Copyright (C) 2021-TODAY Akretion
|
|
# @author Magno Costa <magno.costa@akretion.com.br>
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
from odoo import api, fields, models
|
|
|
|
|
|
class StockInvoiceOnshipping(models.TransientModel):
|
|
_inherit = "stock.invoice.onshipping"
|
|
|
|
@api.model
|
|
def _default_has_down_payment(self):
|
|
pickings = self._load_pickings()
|
|
sale_pickings = pickings.filtered(lambda pk: pk.sale_id)
|
|
downpayment_lines = False
|
|
if sale_pickings:
|
|
for pick in sale_pickings:
|
|
# order = pick.sale_id
|
|
# sale_lines = order.mapped("order_line")
|
|
if pick.sale_id.order_line.filtered(lambda ln: ln.is_downpayment):
|
|
downpayment_lines = True
|
|
|
|
return downpayment_lines
|
|
|
|
deduct_down_payments = fields.Boolean("Deduct down payments", default=True)
|
|
has_down_payments = fields.Boolean(
|
|
"Has down payments", default=_default_has_down_payment
|
|
)
|
|
|
|
def _get_fields_not_used_from_sale(self):
|
|
"""Fields not used from Sale 'prepare' method"""
|
|
# For reference, fields get from 'prepare' method
|
|
# "ref": self.client_order_ref or ''
|
|
# "narration": self.note,
|
|
# "campaign_id": self.campaign_id.id,
|
|
# "medium_id": self.medium_id.id,
|
|
# "source_id": self.source_id.id,
|
|
# "team_id": self.team_id.id,
|
|
# "partner_shipping_id": self.partner_shipping_id.id,
|
|
# "partner_bank_id": self.company_id.partner_id.bank_ids.
|
|
# filtered(lambda bank: bank.company_id.id in
|
|
# (self.company_id.id, False))[:1].id,
|
|
# "invoice_payment_term_id": self.payment_term_id.id,
|
|
# "payment_reference": self.reference,
|
|
# "transaction_ids": [(6, 0, self.transaction_ids.ids)],
|
|
|
|
return {
|
|
"move_type",
|
|
"currency_id",
|
|
"user_id",
|
|
"invoice_user_id",
|
|
"partner_id",
|
|
"fiscal_position_id",
|
|
"journal_id", # company comes from the journal
|
|
"invoice_origin",
|
|
"invoice_line_ids",
|
|
"company_id",
|
|
# Another fields
|
|
"__last_update",
|
|
"display_name",
|
|
}
|
|
|
|
def _build_invoice_values_from_pickings(self, pickings):
|
|
invoice, values = super()._build_invoice_values_from_pickings(pickings)
|
|
|
|
sale_pickings = pickings.filtered(lambda pk: pk.sale_id)
|
|
# Refund case don't get values from Sale Dict
|
|
# TODO: Should get any value?
|
|
if sale_pickings and self._get_invoice_type() != "out_refund":
|
|
# Case more than one Sale Order the fields below will be join
|
|
# the others will be overwritting, as done in sale module,
|
|
# one more field include here Note/Narration
|
|
payment_refs = set()
|
|
refs = set()
|
|
# Include Narration
|
|
narration = set()
|
|
for pick in sale_pickings.sorted(key=lambda p: p.name):
|
|
# Other modules can included new fields in Sale Order and include
|
|
# this fields in the dict of creation Invoice from sale, for
|
|
# example:
|
|
# - account_payment_sale
|
|
# https://github.com/OCA/bank-payment/blob/14.0/
|
|
# account_payment_sale/models/sale_order.py#L41
|
|
# - sale_commssion
|
|
# https://github.com/OCA/commission/blob/14.0/
|
|
# sale_commission/models/sale_order.py#L64
|
|
# To avoid the necessity of a 'glue' module the method get the
|
|
# values from _prepare_invoice but removed some fields of the
|
|
# original method, given priority for values from
|
|
# stock_picking_invoicing dict, for now it's seems the best to
|
|
# way to avoid the 'glue' modules problem.
|
|
sale_values = pick.sale_id._prepare_invoice()
|
|
# Fields to Join
|
|
# origins.add(sale_values["invoice_origin"])
|
|
payment_refs.add(sale_values["payment_reference"])
|
|
refs.add(sale_values["ref"])
|
|
narration.add(sale_values["narration"])
|
|
|
|
sale_values_rm = {
|
|
k: sale_values[k]
|
|
for k in set(sale_values) - self._get_fields_not_used_from_sale()
|
|
}
|
|
|
|
values.update(sale_values_rm)
|
|
|
|
# Fields to join
|
|
if len(sale_pickings) > 1:
|
|
values.update(
|
|
{
|
|
"ref": ", ".join(refs)[:2000],
|
|
# In this case Origin get Pickings Names
|
|
# "invoice_origin": ", ".join(origins),
|
|
"payment_reference": len(payment_refs) == 1
|
|
and payment_refs.pop()
|
|
or False,
|
|
"narration": ", ".join(narration),
|
|
}
|
|
)
|
|
|
|
return invoice, values
|
|
|
|
# Check the comment below
|
|
# def _get_picking_key(self, picking):
|
|
# key = super()._get_picking_key(picking)
|
|
# if picking.sale_id:
|
|
# key = key + (
|
|
# picking.sale_id.payment_term_id,
|
|
# picking.sale_id.fiscal_position_id,
|
|
# picking.sale_id.commitment_date,
|
|
# picking.sale_id.analytic_account_id,
|
|
# picking.sale_id.pricelist_id,
|
|
# picking.sale_id.company_id,
|
|
# )
|
|
# return key
|
|
|
|
def _get_move_key(self, move):
|
|
key = super()._get_move_key(move)
|
|
if move.sale_line_id:
|
|
# TODO: Analise if Sale Lines should be grouped.
|
|
# For now remains a problem https://github.com/odoo/odoo/pull/77195
|
|
# with field qty_invoiced at sale.order.line, when a invoice line
|
|
# has more than one sale line related the field show the total QTY
|
|
# of those lines, e.g:
|
|
# product_uom_qty | qty_invoiced
|
|
# 2.0 | 4.0
|
|
# key = key + (
|
|
# move.sale_line_id.price_unit,
|
|
# move.sale_line_id.customer_lead,
|
|
# move.sale_line_id.currency_id,
|
|
# move.sale_line_id.tax_id,
|
|
# move.sale_line_id.analytic_tag_ids,
|
|
# )
|
|
key = key + (move.sale_line_id,)
|
|
|
|
return key
|
|
|
|
def _get_fields_not_used_from_sale_line(self):
|
|
"""Fields not used from Sale Line 'prepare' method"""
|
|
# Original fields from sale module
|
|
# Fields do get
|
|
# "sequence": self.sequence,
|
|
# "discount": self.discount,
|
|
# "display_type": self.display_type or 'product'
|
|
# "is_downpayment": self.is_downpayment
|
|
# * optional_values
|
|
# * 'N field' included in _prepare_invoice_line method
|
|
# by another module
|
|
|
|
return {
|
|
# Fields from Move has priority
|
|
"name",
|
|
"product_id",
|
|
"product_uom_id",
|
|
"quantity",
|
|
"price_unit",
|
|
"tax_ids",
|
|
# Already get
|
|
"sale_line_ids",
|
|
"anlytic_distribution",
|
|
# another fields
|
|
"__last_update",
|
|
"display_name",
|
|
}
|
|
|
|
def _get_invoice_line_values(self, moves, invoice_values, invoice):
|
|
values = super()._get_invoice_line_values(moves, invoice_values, invoice)
|
|
move = fields.first(moves)
|
|
if move.sale_line_id:
|
|
# Same make above, get fields informed in Sale Line dict
|
|
sale_line_values = move.sale_line_id._prepare_invoice_line()
|
|
# Vals informed in any case
|
|
values["sale_line_ids"] = [(6, 0, moves.sale_line_id.ids)]
|
|
values["analytic_distribution"] = sale_line_values.get(
|
|
"analytic_distribution"
|
|
)
|
|
# Refund case don't get values from Sale Line Dict
|
|
# TODO: Should get any value?
|
|
if self._get_invoice_type() != "out_refund":
|
|
# Same make above, get fields informed in Sale Line dict
|
|
sale_line_values = move.sale_line_id._prepare_invoice_line()
|
|
|
|
sale_line_values_rm = {
|
|
k: sale_line_values[k]
|
|
for k in set(sale_line_values)
|
|
- self._get_fields_not_used_from_sale_line()
|
|
}
|
|
values.update(sale_line_values_rm)
|
|
|
|
return values
|
|
|
|
def _get_pickings_with_sale(self, invoice_values):
|
|
pickings = self._load_pickings()
|
|
|
|
# Filter Picking with Sales
|
|
picking_in_invoice_values = self.env["stock.picking"]
|
|
for line in invoice_values.get("invoice_line_ids"):
|
|
if line[2]:
|
|
if len(line[2].get("move_line_ids")[0]) == 2:
|
|
# [(4, 233)],
|
|
move_line_id = line[2].get("move_line_ids")[0][1]
|
|
else:
|
|
# [(<Command.SET: 6>, 0, [51])]
|
|
move_line_id = line[2].get("move_line_ids")[0][2]
|
|
|
|
move_line = self.env["stock.move"].browse(move_line_id)
|
|
picking_in_invoice_values |= move_line.mapped("picking_id")
|
|
|
|
sale_pickings = pickings.filtered(
|
|
lambda pk: pk.sale_id
|
|
# Check Sales Ungrouped
|
|
and pk.id in picking_in_invoice_values.ids
|
|
)
|
|
|
|
return sale_pickings
|
|
|
|
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
|
|
"""
|
|
sale_pickings = self._get_pickings_with_sale(invoice_values)
|
|
|
|
# Refund case don't included Section, Note or DownPayments
|
|
if not sale_pickings or self._get_invoice_type() == "out_refund":
|
|
return super()._create_invoice(invoice_values)
|
|
|
|
# Check Other Sale Lines
|
|
# Section, Note and Down Payments
|
|
section_note_lines = down_payment_lines = self.env["sale.order.line"]
|
|
# Resequencing
|
|
invoice_item_sequence = (
|
|
0 # Incremental sequencing to keep the lines order on the invoice.
|
|
)
|
|
invoice_item_seq_dict = {}
|
|
for pick in sale_pickings.sorted(key=lambda p: p.name):
|
|
order = pick.sale_id.with_company(pick.sale_id.company_id)
|
|
invoiceable_lines = order._get_invoiceable_lines(final=True)
|
|
section_note_lines |= invoiceable_lines.filtered(
|
|
lambda ln: ln.display_type in ("line_section", "line_note")
|
|
)
|
|
down_payment_lines |= invoiceable_lines.filtered(
|
|
lambda ln: ln.is_downpayment
|
|
)
|
|
|
|
# Use for Resequencing
|
|
for line in order.order_line:
|
|
invoice_item_seq_dict[line.id] = invoice_item_sequence
|
|
invoice_item_sequence += 1
|
|
|
|
# Sections and Notes
|
|
if section_note_lines:
|
|
section_note_vals = []
|
|
for line in section_note_lines:
|
|
sale_line_vals = line._prepare_invoice_line()
|
|
# Change [(4, 59)] for [(6, 0, [59])] to avoid error
|
|
# in method to Resequencing
|
|
sale_line_vals["sale_line_ids"] = [
|
|
(6, 0, [sale_line_vals.get("sale_line_ids")[0][1]])
|
|
]
|
|
section_note_vals.append((0, 0, sale_line_vals))
|
|
|
|
invoice_values["invoice_line_ids"] += section_note_vals
|
|
|
|
# Resequencing, necessary in the case of Grouping Sale Orders
|
|
for line in invoice_values.get("invoice_line_ids"):
|
|
# [(6, 0, {})]
|
|
if line[2]:
|
|
sale_line = line[2].get("sale_line_ids")
|
|
if sale_line:
|
|
# [(6, 0, [58])]
|
|
line[2]["sequence"] = invoice_item_seq_dict.get(sale_line[0][2][0])
|
|
|
|
# Down Payments
|
|
# After the Resequencing to put it in the end of Invoice
|
|
if down_payment_lines:
|
|
down_payment_vals = []
|
|
down_payment_section_added = False
|
|
for line in down_payment_lines:
|
|
if not down_payment_section_added and line.is_downpayment:
|
|
# Create a dedicated section for the down payments
|
|
# (put at the end of the invoiceable_lines)
|
|
down_payment_vals.append(
|
|
(
|
|
0,
|
|
0,
|
|
line.order_id._prepare_down_payment_section_line(
|
|
sequence=invoice_item_sequence,
|
|
),
|
|
),
|
|
)
|
|
down_payment_section_added = True
|
|
invoice_item_sequence += 1
|
|
|
|
if line.is_downpayment:
|
|
down_payment_vals.append(
|
|
(
|
|
0,
|
|
0,
|
|
line._prepare_invoice_line(
|
|
sequence=invoice_item_sequence,
|
|
),
|
|
),
|
|
)
|
|
invoice_item_sequence += 1
|
|
|
|
invoice_values["invoice_line_ids"] += down_payment_vals
|
|
|
|
moves = (
|
|
self.env["account.move"]
|
|
.sudo()
|
|
.with_context(default_move_type="out_invoice")
|
|
.create(invoice_values)
|
|
)
|
|
|
|
# param Final: if True, refunds will be generated if necessary
|
|
final = self.deduct_down_payments
|
|
if final:
|
|
moves.sudo().filtered(
|
|
lambda m: m.amount_total < 0
|
|
).action_switch_move_type()
|
|
for move in moves:
|
|
move.message_post_with_source(
|
|
source_ref="mail.message_origin_link",
|
|
render_values={
|
|
"self": move.picking_ids,
|
|
"origin": move.picking_ids,
|
|
},
|
|
subtype_id=self.env.ref("mail.mt_note").id,
|
|
)
|
|
return moves
|