Files
tradon/modules/purchase_trade/invoice.py
2026-05-20 14:29:31 +02:00

1682 lines
60 KiB
Python

from decimal import Decimal, ROUND_HALF_UP
from datetime import date as dt_date
from sql import Literal
from sql.conditionals import Case
from sql.functions import CurrentTimestamp
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool, PoolMeta
from trytond.modules.purchase_trade.numbers_to_words import amount_to_currency_words
from trytond.exceptions import UserError
from trytond.transaction import Transaction
from trytond.modules.account_invoice.invoice import (
InvoiceReport as BaseInvoiceReport)
from trytond.modules.sale.sale import SaleReport as BaseSaleReport
from trytond.modules.purchase.purchase import (
PurchaseReport as BasePurchaseReport)
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
def do_lot_invoicing(self):
super().do_lot_invoicing()
self._create_sale_padding_moves()
@classmethod
def _post(cls, invoices):
pool = Pool()
Move = pool.get('account.move')
super()._post(invoices)
padding_moves = []
for invoice in invoices:
padding_moves.extend(invoice._create_sale_padding_moves())
padding_moves.extend([
move for move in (invoice.additional_moves or [])
if move.description in {
invoice._get_sale_padding_move_description(False),
invoice._get_sale_padding_move_description(True),
}
and move.state != 'posted'
])
if padding_moves:
cls.save(invoices)
Move.post([m for m in padding_moves if m.state != 'posted'])
def _get_sale_padding_accounts(self):
Configuration = Pool().get('account.configuration')
config = Configuration(1)
sale_account = config.get_multivalue(
'default_sale_padding_account', company=self.company.id)
accrual_account = config.get_multivalue(
'default_accrual_padding_account', company=self.company.id)
if not sale_account or not accrual_account:
raise UserError(
'Default Sale Padding and Default Accrual Padding '
'accounts must be configured.')
return sale_account, accrual_account
def _has_sale_padding_move(self, reversal=False):
description = self._get_sale_padding_move_description(reversal)
return any(
move.description == description
for move in (self.additional_moves or []))
@staticmethod
def _get_sale_padding_move_description(reversal=False):
if reversal:
return 'Sale padding reversal'
return 'Sale padding accrual'
def _get_padding_company_amount(self, invoice_line, padding):
Currency = Pool().get('currency.currency')
invoice = invoice_line.invoice
amount = Decimal(str(padding or 0)) * Decimal(
str(invoice_line.unit_price or 0))
amount = invoice.currency.round(amount)
if invoice.currency == invoice.company.currency:
return amount, amount
if invoice.rate:
company_amount = invoice.company.currency.round(
amount / invoice.rate)
else:
with Transaction().set_context(date=invoice.currency_date):
company_amount = Currency.compute(
invoice.currency, amount, invoice.company.currency)
return amount, company_amount
def _get_sale_padding_entries(self):
if self.type != 'out':
return []
entries = []
for line in self.lines or []:
if getattr(line, 'type', None) != 'line':
continue
lot = getattr(line, 'lot', None)
if not lot:
continue
padding = Decimal(str(
getattr(lot, 'sale_invoice_padding', 0) or 0))
if padding <= 0:
continue
if self.reference == 'Provisional' and line.description == 'Pro forma':
entries.append((lot, line, padding, False))
elif self.reference == 'Final' and line.description == 'Final':
provisional_line = getattr(lot, 'sale_invoice_line_prov', None)
if provisional_line:
entries.append((lot, provisional_line, padding, True))
return entries
def _get_sale_padding_move_lines(self, entries):
MoveLine = Pool().get('account.move.line')
sale_account, accrual_account = self._get_sale_padding_accounts()
move_lines = []
for lot, invoice_line, padding, reversal in entries:
padding_amount, company_amount = self._get_padding_company_amount(
invoice_line, padding)
if not company_amount:
continue
sale_line = MoveLine()
accrual_line = MoveLine()
for move_line in (sale_line, accrual_line):
move_line.lot = lot
move_line.origin = invoice_line
move_line.description = 'Padding'
if not reversal:
sale_line.account = sale_account
sale_line.debit = company_amount
sale_line.credit = Decimal(0)
accrual_line.account = accrual_account
accrual_line.debit = Decimal(0)
accrual_line.credit = company_amount
else:
accrual_line.account = accrual_account
accrual_line.debit = company_amount
accrual_line.credit = Decimal(0)
sale_line.account = sale_account
sale_line.debit = Decimal(0)
sale_line.credit = company_amount
if self.currency != self.company.currency:
sale_line.second_currency = self.currency
accrual_line.second_currency = self.currency
sale_line.amount_second_currency = padding_amount.copy_sign(
sale_line.debit - sale_line.credit)
accrual_line.amount_second_currency = padding_amount.copy_sign(
accrual_line.debit - accrual_line.credit)
if sale_line.account.party_required:
sale_line.party = self.party
if accrual_line.account.party_required:
accrual_line.party = self.party
move_lines.extend([sale_line, accrual_line])
return move_lines
def _create_sale_padding_moves(self):
pool = Pool()
Move = pool.get('account.move')
Period = pool.get('account.period')
Date = pool.get('ir.date')
entries = self._get_sale_padding_entries()
if not entries:
return []
reversal = entries[0][3]
if self._has_sale_padding_move(reversal):
return []
move_lines = self._get_sale_padding_move_lines(entries)
if not move_lines:
return []
with Transaction().set_context(company=self.company.id):
today = Date.today()
accounting_date = self.accounting_date or self.invoice_date or today
period = Period.find(self.company, date=accounting_date)
move = Move()
move.journal = self.journal
move.period = period
move.date = accounting_date
move.origin = self
move.company = self.company
move.description = self._get_sale_padding_move_description(reversal)
move.lines = move_lines
Move.save([move])
self.additional_moves = tuple(self.additional_moves or ()) + (move,)
return [move]
@staticmethod
def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False,
strip_trailing_zeros=True):
value = Decimal(str(value or 0)).quantize(Decimal(digits))
text = format(value, 'f')
if strip_trailing_zeros:
text = text.rstrip('0').rstrip('.')
if keep_trailing_decimal and '.' not in text:
text += '.0'
return text or '0'
@classmethod
def _format_report_quantity_display(cls, value):
return cls._format_report_number(
value, digits='0.01', strip_trailing_zeros=False)
def _get_report_invoice_line(self):
for line in self.lines or []:
if getattr(line, 'type', None) == 'line':
return line
return self.lines[0] if self.lines else None
def _get_report_invoice_lines(self):
lines = [
line for line in (self.lines or [])
if getattr(line, 'type', None) == 'line'
]
return lines or list(self.lines or [])
@staticmethod
def _get_report_related_lots(line):
lots = []
seen = set()
def add_lot(lot):
if not lot:
return
lot_id = getattr(lot, 'id', None)
key = ('id', lot_id) if lot_id is not None else ('obj', id(lot))
if key in seen:
return
seen.add(key)
lots.append(lot)
add_lot(getattr(line, 'lot', None))
origin = getattr(line, 'origin', None)
for lot in getattr(origin, 'lots', []) or []:
add_lot(lot)
return lots
@classmethod
def _get_report_preferred_lots(cls, line):
lots = cls._get_report_related_lots(line)
physicals = [
lot for lot in lots
if getattr(lot, 'lot_type', None) == 'physic'
]
if physicals:
return physicals
virtuals = [
lot for lot in lots
if getattr(lot, 'lot_type', None) == 'virtual'
]
if len(virtuals) == 1:
return virtuals
return []
@staticmethod
def _get_report_line_sign(line):
quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
return Decimal(-1) if quantity < 0 else Decimal(1)
@staticmethod
def _get_report_lot_hist_weights(lot):
if not lot:
return None, None
if hasattr(lot, 'get_hist_quantity'):
net, gross = lot.get_hist_quantity()
return (
Decimal(str(net or 0)),
Decimal(str(gross if gross not in (None, '') else net or 0)),
)
hist = list(getattr(lot, 'lot_hist', []) or [])
state = getattr(lot, 'lot_state', None)
state_id = getattr(state, 'id', None)
if state_id is not None:
for entry in hist:
quantity_type = getattr(entry, 'quantity_type', None)
if getattr(quantity_type, 'id', None) == state_id:
net = Decimal(str(getattr(entry, 'quantity', 0) or 0))
gross = Decimal(str(
getattr(entry, 'gross_quantity', None)
if getattr(entry, 'gross_quantity', None) not in (None, '')
else net))
return net, gross
return None, None
def _get_report_invoice_line_weights(self, line):
lots = self._get_report_preferred_lots(line)
if lots and self._report_invoice_line_reuses_lot(line):
quantity = self._get_report_invoice_line_quantity_from_line(line)
return quantity, quantity
if lots:
sign = self._get_report_line_sign(line)
net_total = Decimal(0)
gross_total = Decimal(0)
for lot in lots:
net, gross = self._get_report_lot_hist_weights(lot)
if net is None:
continue
net_total += net
gross_total += gross
if net_total or gross_total:
return net_total * sign, gross_total * sign
quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
return quantity, quantity
@staticmethod
def _get_report_line_lot_keys(line):
keys = []
for lot in Invoice._get_report_preferred_lots(line):
lot_id = getattr(lot, 'id', None)
keys.append(lot_id if lot_id is not None else id(lot))
return tuple(sorted(keys))
def _report_invoice_line_reuses_lot(self, line):
line_keys = self._get_report_line_lot_keys(line)
if not line_keys:
return False
for other in self._get_report_invoice_lines():
if other is line:
continue
if self._get_report_line_lot_keys(other) == line_keys:
return True
return False
def _get_report_reused_lot_lines(self):
groups = {}
for line in self._get_report_invoice_lines():
lots = self._get_report_preferred_lots(line)
if not lots:
continue
for lot in lots:
lot_id = getattr(lot, 'id', None)
key = lot_id if lot_id is not None else id(lot)
groups.setdefault(key, {'lot': lot, 'lines': []})
groups[key]['lines'].append(line)
return {
key: value for key, value in groups.items()
if len(value['lines']) > 1
}
@staticmethod
def _convert_report_quantity(quantity, from_unit, to_unit):
value = Decimal(str(quantity or 0))
if not from_unit or not to_unit:
return value
if getattr(from_unit, 'id', None) == getattr(to_unit, 'id', None):
return value
from_name = getattr(from_unit, 'rec_name', None)
to_name = getattr(to_unit, 'rec_name', None)
if from_name and to_name and from_name == to_name:
return value
converted = Pool().get('product.uom').compute_qty(
from_unit, float(value), to_unit) or 0
return Decimal(str(converted))
@classmethod
def _get_report_invoice_line_quantity_from_line(cls, line):
quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
return cls._convert_report_quantity(
quantity,
getattr(line, 'unit', None),
cls._get_report_invoice_line_unit(line),
)
@classmethod
def _find_report_hist_entry_for_quantity(cls, lot, quantity, exclude_state_id=None):
target = Decimal(str(quantity or 0)).copy_abs()
for entry in list(getattr(lot, 'lot_hist', []) or []):
quantity_type = getattr(entry, 'quantity_type', None)
if getattr(quantity_type, 'id', None) == exclude_state_id:
continue
entry_quantity = Decimal(str(getattr(entry, 'quantity', 0) or 0))
if entry_quantity == target:
return entry
return None
def _get_report_reused_lot_gross_total(self):
reused_lots = self._get_report_reused_lot_lines()
if not reused_lots:
return None
total = Decimal(0)
for data in reused_lots.values():
lot = data['lot']
_, current_gross = self._get_report_lot_hist_weights(lot)
if current_gross is None:
continue
current_state = getattr(getattr(lot, 'lot_state', None), 'id', None)
negative_lines = [
line for line in data['lines']
if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0
]
previous_gross = Decimal(0)
matched = False
for line in negative_lines:
previous_entry = self._find_report_hist_entry_for_quantity(
lot,
self._get_report_invoice_line_quantity_from_line(line),
exclude_state_id=current_state,
)
if not previous_entry:
continue
previous_gross += Decimal(str(
getattr(previous_entry, 'gross_quantity', 0) or 0))
matched = True
if matched:
total += current_gross - previous_gross
return total
@staticmethod
def _get_report_invoice_line_unit(line):
lots = Invoice._get_report_preferred_lots(line)
if lots and getattr(lots[0], 'lot_unit_line', None):
return lots[0].lot_unit_line
return getattr(line, 'unit', None)
@staticmethod
def _get_report_lbs_unit():
Uom = Pool().get('product.uom')
for domain in (
[('symbol', '=', 'LBS')],
[('rec_name', '=', 'LBS')],
[('name', '=', 'LBS')],
[('symbol', '=', 'LB')],
[('rec_name', '=', 'LB')],
[('name', '=', 'LB')]):
units = Uom.search(domain, limit=1)
if units:
return units[0]
return None
@classmethod
def _convert_report_quantity_to_lbs(cls, quantity, unit):
value = Decimal(str(quantity or 0))
if value == 0:
return Decimal('0.00')
if not unit:
return (value * Decimal('2204.62')).quantize(Decimal('0.01'))
label = (
getattr(unit, 'symbol', None)
or getattr(unit, 'rec_name', None)
or getattr(unit, 'name', None)
or ''
).strip().upper()
if label in {'LBS', 'LB', 'POUND', 'POUNDS'}:
return value.quantize(Decimal('0.01'))
lbs_unit = cls._get_report_lbs_unit()
if lbs_unit:
converted = Pool().get('product.uom').compute_qty(
unit, float(value), lbs_unit) or 0
return Decimal(str(converted)).quantize(Decimal('0.01'))
if label in {'KG', 'KGS', 'KILOGRAM', 'KILOGRAMS'}:
return (value * Decimal('2.20462')).quantize(Decimal('0.01'))
return (value * Decimal('2204.62')).quantize(Decimal('0.01'))
@staticmethod
def _clean_report_description(value):
text = (value or '').strip()
normalized = text.replace(' ', '').upper()
if normalized == 'PROFORMA':
return ''
return text.upper() if text else ''
def _get_report_purchase(self):
purchases = list(self.purchases or [])
return purchases[0] if purchases else None
def _get_report_sale(self):
# Bridge invoice templates to the originating sale so FODT files can
# reuse stable sale.report_* properties instead of complex expressions.
sales = list(self.sales or [])
return sales[0] if sales else None
def _get_report_trade(self):
return self._get_report_sale() or self._get_report_purchase()
def _get_report_deal_sale(self):
sale = self._get_report_sale()
if sale:
return sale
for lot in self._get_report_invoice_lots() or []:
line = getattr(lot, 'sale_line', None)
sale = getattr(line, 'sale', None) if line else None
if sale:
return sale
def _get_report_purchase_line(self):
purchase = self._get_report_purchase()
if purchase and purchase.lines:
return purchase.lines[0]
def _get_report_sale_line(self):
sale = self._get_report_sale()
if sale and sale.lines:
return sale.lines[0]
def _get_report_trade_line(self):
return self._get_report_sale_line() or self._get_report_purchase_line()
def _get_report_lot(self):
line = self._get_report_trade_line()
if line and line.lots:
for lot in line.lots:
if lot.lot_type == 'physic':
return lot
return line.lots[0]
@staticmethod
def _get_report_lot_shipment(lot):
if not lot:
return None
return (
getattr(lot, 'lot_shipment_in', None)
or getattr(lot, 'lot_shipment_out', None)
or getattr(lot, 'lot_shipment_internal', None)
)
def _get_report_invoice_shipments(self):
shipments = []
seen = set()
for line in self._get_report_invoice_lines():
for lot in self._get_report_preferred_lots(line):
shipment = self._get_report_lot_shipment(lot)
if not shipment:
continue
shipment_id = getattr(shipment, 'id', None)
key = (
getattr(shipment, '__name__', None),
shipment_id if shipment_id is not None else id(shipment),
)
if key in seen:
continue
seen.add(key)
shipments.append(shipment)
return shipments
def _get_report_invoice_lots(self):
invoice_lines = self._get_report_invoice_lines()
if not invoice_lines:
return []
def _same_invoice_line(left, right):
if not left or not right:
return False
left_id = getattr(left, 'id', None)
right_id = getattr(right, 'id', None)
if left_id is not None and right_id is not None:
return left_id == right_id
return left is right
trade = self._get_report_trade()
trade_lines = getattr(trade, 'lines', []) if trade else []
lots = []
for line in trade_lines or []:
for lot in getattr(line, 'lots', []) or []:
if getattr(lot, 'lot_type', None) != 'physic':
continue
refs = [
getattr(lot, 'sale_invoice_line', None),
getattr(lot, 'sale_invoice_line_prov', None),
getattr(lot, 'invoice_line', None),
getattr(lot, 'invoice_line_prov', None),
]
if any(
_same_invoice_line(ref, invoice_line)
for ref in refs for invoice_line in invoice_lines):
lots.append(lot)
return lots
@staticmethod
def _format_report_package_label(unit):
label = (
getattr(unit, 'symbol', None)
or getattr(unit, 'rec_name', None)
or getattr(unit, 'name', None)
or 'BALE'
)
label = label.upper()
if not label.endswith('S'):
label += 'S'
return label
def _get_report_freight_fee(self):
pool = Pool()
Fee = pool.get('fee.fee')
shipment = self._get_report_shipment()
if not shipment:
return None
fees = Fee.search([
('shipment_in', '=', shipment.id),
('product.name', '=', 'Maritime freight'),
], limit=1)
return fees[0] if fees else None
def _get_report_shipment(self):
shipments = self._get_report_invoice_shipments()
if len(shipments) == 1:
return shipments[0]
if len(shipments) > 1:
return None
lot = self._get_report_lot()
return self._get_report_lot_shipment(lot)
@staticmethod
def _get_report_bank_account(party):
accounts = list(getattr(party, 'bank_accounts', []) or [])
return accounts[0] if accounts else None
@staticmethod
def _get_report_bank_account_number(account):
if not account:
return ''
numbers = list(getattr(account, 'numbers', []) or [])
for number in numbers:
if getattr(number, 'type', None) == 'iban' and getattr(number, 'number', None):
return number.number or ''
for number in numbers:
if getattr(number, 'number', None):
return number.number or ''
return ''
@staticmethod
def _get_report_bank_name(account):
bank = getattr(account, 'bank', None) if account else None
party = getattr(bank, 'party', None) if bank else None
return getattr(party, 'rec_name', None) or getattr(bank, 'rec_name', None) or ''
@staticmethod
def _get_report_bank_city(account):
bank = getattr(account, 'bank', None) if account else None
party = getattr(bank, 'party', None) if bank else None
address = party.address_get() if party and hasattr(party, 'address_get') else None
return getattr(address, 'city', None) or ''
@staticmethod
def _get_report_bank_swift(account):
bank = getattr(account, 'bank', None) if account else None
return getattr(bank, 'bic', None) or ''
@staticmethod
def _format_report_payment_amount(value):
amount = Decimal(str(value or 0)).quantize(Decimal('0.01'))
return format(amount, 'f')
@property
def _report_payment_order_company_account(self):
return self._get_report_bank_account(getattr(self.company, 'party', None))
@property
def _report_payment_order_beneficiary_account(self):
return self._get_report_bank_account(self.party)
@property
def report_payment_order_short_name(self):
company_party = getattr(self.company, 'party', None)
return getattr(company_party, 'rec_name', '') or ''
@property
def report_payment_order_document_reference(self):
return self.number or self.reference or ''
@property
def report_payment_order_from_account_nb(self):
return self._get_report_bank_account_number(
self._report_payment_order_company_account)
@property
def report_payment_order_to_bank_name(self):
return self._get_report_bank_name(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_to_bank_city(self):
return self._get_report_bank_city(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_amount(self):
return self._format_report_payment_amount(self.total_amount)
@property
def report_payment_order_currency_code(self):
currency = self.currency
code = getattr(currency, 'code', None) or ''
rec_name = getattr(currency, 'rec_name', None) or ''
symbol = getattr(currency, 'symbol', None) or ''
if code and any(ch.isalpha() for ch in code):
return code
if rec_name and any(ch.isalpha() for ch in rec_name):
return rec_name
if symbol and any(ch.isalpha() for ch in symbol):
return symbol
return code or rec_name or symbol or ''
@property
def report_payment_order_amount_text(self):
return amount_to_currency_words(self.total_amount)
@property
def report_payment_order_value_date(self):
value_date = self.payment_term_date or self.invoice_date
if isinstance(value_date, dt_date):
return value_date.strftime('%d-%m-%Y')
return ''
@property
def report_payment_order_company_address(self):
if self.invoice_address and getattr(self.invoice_address, 'full_address', None):
return self.invoice_address.full_address
return self.report_address
@property
def report_payment_order_beneficiary_account_nb(self):
return self._get_report_bank_account_number(
self._report_payment_order_beneficiary_account)
@property
def report_payment_order_beneficiary_bank_name(self):
return self._get_report_bank_name(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_beneficiary_bank_city(self):
return self._get_report_bank_city(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_swift_code(self):
return self._get_report_bank_swift(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_other_instructions(self):
return self.description or ''
@property
def report_payment_order_reference(self):
return self.reference or self.number or ''
@staticmethod
def _get_report_current_user():
user_id = Transaction().user
if not user_id:
return None
User = Pool().get('res.user')
return User(user_id)
@property
def report_payment_order_current_user(self):
user = self._get_report_current_user()
return getattr(user, 'rec_name', None) or ''
@property
def report_payment_order_current_user_email(self):
user = self._get_report_current_user()
party = getattr(user, 'party', None) if user else None
if party and hasattr(party, 'contact_mechanism_get'):
return party.contact_mechanism_get('email') or ''
return getattr(user, 'email', None) or ''
@property
def report_address(self):
trade = self._get_report_trade()
if trade and trade.report_address:
return trade.report_address
if self.invoice_address and self.invoice_address.full_address:
return self.invoice_address.full_address
return ''
@property
def report_contract_number(self):
sale = self._get_report_deal_sale()
deal = getattr(sale, 'report_deal', None) if sale else None
if deal:
return deal.replace(' ', '/')
trade = self._get_report_trade()
if trade and trade.full_number:
return trade.full_number
return self.origins or ''
@property
def report_shipment(self):
trade = self._get_report_trade()
if trade and trade.report_shipment:
return trade.report_shipment
return self.description or ''
@property
def report_trader_initial(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'trader', None):
return trade.trader.initial or ''
return ''
@property
def report_origin(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'product_origin', None):
return trade.product_origin or ''
return ''
@property
def report_operator_initial(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'operator', None):
return trade.operator.initial or ''
return ''
@property
def report_product_description(self):
line = self._get_report_trade_line()
if line and line.product:
return line.product.description or ''
return ''
@property
def report_product_name(self):
line = self._get_report_trade_line()
if line and line.product:
return line.product.name or ''
return ''
@property
def report_description_upper(self):
if self.lines:
return self._clean_report_description(self.lines[0].description)
return ''
@property
def report_crop_name(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'crop', None):
return trade.crop.name or ''
return ''
@property
def report_attributes_name(self):
line = self._get_report_trade_line()
if line:
return getattr(line, 'attributes_name', '') or ''
return ''
@property
def report_price(self):
trade = self._get_report_trade()
if trade and trade.report_price:
return trade.report_price
return ''
@property
def report_quantity_lines(self):
details = []
for line in self._get_report_invoice_lines():
quantity, _ = self._get_report_invoice_line_weights(line)
if quantity == '':
continue
quantity_text = self._format_report_quantity_display(quantity)
unit = self._get_report_invoice_line_unit(line)
unit_name = unit.rec_name.upper() if unit and unit.rec_name else ''
lbs = self._convert_report_quantity_to_lbs(quantity, unit)
parts = [quantity_text, unit_name]
if lbs != '':
parts.append(
f"({self._format_report_quantity_display(lbs)} LBS)")
detail = ' '.join(part for part in parts if part)
if detail:
details.append(detail)
return '\n'.join(details)
@property
def report_trade_blocks(self):
blocks = []
quantity_lines = self.report_quantity_lines.splitlines()
rate_lines = self.report_rate_lines.splitlines()
for index, quantity_line in enumerate(quantity_lines):
price_line = rate_lines[index] if index < len(rate_lines) else ''
blocks.append((quantity_line, price_line))
return blocks
@property
def report_rate_currency_upper(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_currency_upper
return ''
@property
def report_rate_value(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_value
return ''
@property
def report_rate_unit_upper(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_unit_upper
return ''
@property
def report_rate_price_words(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_price_words
return self.report_price or ''
@property
def report_rate_pricing_text(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_pricing_text
return ''
@property
def report_rate_lines(self):
rows = self.report_rate_rows
if rows:
return '\n'.join(row['label'] for row in rows)
return ''
@property
def report_rate_rows(self):
price_composition_rows = self._get_report_price_composition_rows()
if price_composition_rows:
return price_composition_rows
return [{
'label': line,
'amount': None,
} for line in self._get_report_rate_line_details()]
def _get_report_rate_line_details(self):
details = []
for line in self._get_report_invoice_lines():
currency = getattr(line, 'report_rate_currency_upper', '') or ''
value = getattr(line, 'report_rate_value', '')
value_text = ''
if value != '':
value_text = self._format_report_number(
value, strip_trailing_zeros=False)
unit = getattr(line, 'report_rate_unit_upper', '') or ''
words = getattr(line, 'report_rate_price_words', '') or ''
pricing_text = getattr(line, 'report_rate_pricing_text', '') or ''
detail = ' '.join(
part for part in [
currency,
value_text,
'PER' if unit else '',
unit,
f"({words})" if words else '',
pricing_text,
] if part)
if detail:
details.append(detail)
return details
def _get_report_price_composition_rows(self):
sale = self._get_report_sale()
if sale and getattr(sale, 'report_price_composition_rows', None):
return sale.report_price_composition_rows
for line in self._get_report_invoice_lines():
origin = getattr(line, 'origin', None)
sale = getattr(origin, 'sale', None)
if sale and getattr(sale, 'report_price_composition_rows', None):
return sale.report_price_composition_rows
return []
@property
def report_positive_rate_lines(self):
sale = self._get_report_sale()
if sale and getattr(sale, 'report_price_lines', None):
return sale.report_price_lines
details = []
for line in self._get_report_invoice_lines():
quantity = getattr(line, 'report_net', '')
if quantity == '':
quantity = getattr(line, 'quantity', '')
if Decimal(str(quantity or 0)) <= 0:
continue
currency = getattr(line, 'report_rate_currency_upper', '') or ''
value = getattr(line, 'report_rate_value', '')
value_text = ''
if value != '':
value_text = self._format_report_number(
value, strip_trailing_zeros=False)
unit = getattr(line, 'report_rate_unit_upper', '') or ''
words = getattr(line, 'report_rate_price_words', '') or ''
pricing_text = getattr(line, 'report_rate_pricing_text', '') or ''
detail = ' '.join(
part for part in [
currency,
value_text,
'PER' if unit else '',
unit,
f"({words})" if words else '',
pricing_text,
] if part)
if detail:
details.append(detail)
return '\n'.join(details)
@property
def report_payment_date(self):
trade = self._get_report_trade()
if trade and trade.report_payment_date:
return trade.report_payment_date
return ''
@property
def report_delivery_period_description(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'report_delivery_period_description', None):
return trade.report_delivery_period_description
line = self._get_report_trade_line()
if line and getattr(line, 'del_period', None):
return line.del_period.description or ''
return ''
@property
def report_payment_description(self):
trade = self._get_report_trade()
if trade and trade.payment_term:
return trade.payment_term.description or ''
if self.payment_term:
return self.payment_term.description or ''
return ''
@property
def report_nb_bale(self):
total_packages = Decimal(0)
package_unit = None
has_invoice_line_packages = False
for line in self._get_report_invoice_lines():
lot = getattr(line, 'lot', None)
if not lot or getattr(lot, 'lot_qt', None) in (None, ''):
continue
has_invoice_line_packages = True
if not package_unit and getattr(lot, 'lot_unit', None):
package_unit = lot.lot_unit
sign = Decimal(1)
if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0:
sign = Decimal(-1)
total_packages += (
Decimal(str(lot.lot_qt or 0)).quantize(
Decimal('1'), rounding=ROUND_HALF_UP) * sign)
if has_invoice_line_packages:
label = self._format_report_package_label(package_unit)
return f"NB {label}: {int(total_packages)}"
lots = self._get_report_invoice_lots()
if lots:
total_packages = Decimal(0)
package_unit = None
for lot in lots:
if getattr(lot, 'lot_qt', None):
total_packages += Decimal(str(lot.lot_qt or 0))
if not package_unit and getattr(lot, 'lot_unit', None):
package_unit = lot.lot_unit
package_qty = total_packages.quantize(
Decimal('1'), rounding=ROUND_HALF_UP)
label = self._format_report_package_label(package_unit)
return f"NB {label}: {int(package_qty)}"
sale = self._get_report_sale()
if sale and sale.report_nb_bale:
return sale.report_nb_bale
line = self._get_report_trade_line()
if line and line.lots:
nb_bale = sum(
lot.lot_qt for lot in line.lots if lot.lot_type == 'physic'
)
return 'NB BALES: ' + str(int(nb_bale))
return ''
@property
def report_cndn_nb_bale(self):
nb_bale = self.report_nb_bale
if nb_bale == 'NB BALES: 0':
return 'Unchanged'
return nb_bale
@property
def report_net_display(self):
net = self.report_net
if net == '':
return ''
return self._format_report_quantity_display(net)
@property
def report_gross_display(self):
gross = self.report_gross
if gross == '':
return ''
return self._format_report_quantity_display(gross)
@property
def report_lbs_display(self):
lbs = self.report_lbs
if lbs == '':
return ''
return self._format_report_quantity_display(lbs)
@property
def report_gross(self):
if self.lines:
reused_gross = self._get_report_reused_lot_gross_total()
if reused_gross is not None:
non_reused_total = sum(
self._get_report_invoice_line_weights(line)[1]
for line in self._get_report_invoice_lines()
if not self._report_invoice_line_reuses_lot(line))
return non_reused_total + reused_gross
return sum(
self._get_report_invoice_line_weights(line)[1]
for line in self._get_report_invoice_lines())
line = self._get_report_trade_line()
if line and line.lots:
return sum(
lot.get_current_gross_quantity()
for lot in line.lots if lot.lot_type == 'physic'
)
return ''
@property
def report_net(self):
if self.lines:
return sum(
self._get_report_invoice_line_weights(line)[0]
for line in self._get_report_invoice_lines())
line = self._get_report_trade_line()
if line and line.lots:
return sum(
lot.get_current_quantity()
for lot in line.lots if lot.lot_type == 'physic'
)
if self.lines:
return self.lines[0].quantity
return ''
@property
def report_lbs(self):
net = self.report_net
if net == '':
return ''
invoice_line = self._get_report_invoice_line()
unit = self._get_report_invoice_line_unit(invoice_line) if invoice_line else None
return self._convert_report_quantity_to_lbs(net, unit)
@property
def report_weight_unit_upper(self):
invoice_line = self._get_report_invoice_line()
unit = self._get_report_invoice_line_unit(invoice_line) if invoice_line else None
if not unit:
line = self._get_report_trade_line()
lot = self._get_report_lot()
unit = (
getattr(lot, 'lot_unit_line', None)
or getattr(line, 'unit', None) if line else None
)
if unit and unit.rec_name:
return unit.rec_name.upper()
return 'KGS'
@property
def report_note_title(self):
total = Decimal(str(self.total_amount or 0))
invoice_type = getattr(self, 'type', None)
if not invoice_type:
if self.sales:
invoice_type = 'out'
elif self.purchases:
invoice_type = 'in'
if invoice_type == 'out':
if total < 0:
return 'Credit Note'
return 'Debit Note'
if total < 0:
return 'Debit Note'
return 'Credit Note'
@property
def report_transportation(self):
shipment = self._get_report_shipment()
if not shipment:
return ''
supplier = getattr(shipment, 'supplier', None)
vessel = getattr(shipment, 'vessel', None)
supplier_name = (
getattr(supplier, 'name', None)
or getattr(supplier, 'rec_name', None)
or '')
vessel_name = getattr(vessel, 'vessel_name', None) or ''
note = getattr(shipment, 'note', None) or ''
if supplier_name and vessel_name:
transport = f"BY {supplier_name} ({vessel_name})"
elif supplier_name:
transport = f"BY {supplier_name}"
else:
transport = vessel_name
return ' '.join(part for part in [transport, note] if part).strip()
@property
def report_bl_date(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.bl_date
@property
def report_bl_nb(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.bl_number
@property
def report_vessel(self):
shipment = self._get_report_shipment()
if shipment and shipment.vessel:
return shipment.vessel.vessel_name
@property
def report_loading_port(self):
shipment = self._get_report_shipment()
if shipment and shipment.from_location:
return shipment.from_location.rec_name
return ''
@property
def report_discharge_port(self):
shipment = self._get_report_shipment()
if shipment and shipment.to_location:
return shipment.to_location.rec_name
return ''
@property
def report_incoterm(self):
trade = self._get_report_trade()
if not trade:
return ''
incoterm = trade.incoterm.code if getattr(trade, 'incoterm', None) else ''
location = (
trade.incoterm_location.party_name
if getattr(trade, 'incoterm_location', None) else ''
)
if incoterm and location:
return f"{incoterm} {location}"
return incoterm or location
@property
def report_proforma_invoice_number(self):
lot = self._get_report_lot()
if lot:
line = (
getattr(lot, 'sale_invoice_line_prov', None)
or getattr(lot, 'invoice_line_prov', None)
)
if line and line.invoice:
return line.invoice.number or ''
return ''
@property
def report_proforma_invoice_date(self):
lot = self._get_report_lot()
if lot:
line = (
getattr(lot, 'sale_invoice_line_prov', None)
or getattr(lot, 'invoice_line_prov', None)
)
if line and line.invoice:
return line.invoice.invoice_date
@property
def report_controller_name(self):
shipment = self._get_report_shipment()
if shipment and shipment.controller:
return shipment.controller.rec_name
return ''
@property
def report_si_number(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.number or ''
return ''
@property
def report_si_reference(self):
shipment = self._get_report_shipment()
if shipment:
return getattr(shipment, 'reference', None) or ''
return ''
@property
def report_freight_amount(self):
fee = self._get_report_freight_fee()
if fee:
return fee.get_amount()
return ''
@property
def report_freight_currency_symbol(self):
fee = self._get_report_freight_fee()
if fee and fee.currency:
return fee.currency.symbol or ''
if self.currency:
return self.currency.symbol or ''
return 'USD'
class InvoicePaddingReport(ModelSQL, ModelView):
"Invoice with padding"
__name__ = 'invoice.padding.report'
party = fields.Many2One('party.party', "Party")
sale = fields.Many2One('sale.sale', "Sale")
sale_line = fields.Many2One('sale.line', "Sale Line")
lot = fields.Many2One('lot.lot', "Lot")
product = fields.Many2One('product.product', "Product")
currency = fields.Many2One('currency.currency', "Currency")
unit = fields.Many2One('product.uom', "Unit")
padding_quantity = fields.Numeric("Padding Quantity", digits='unit')
unit_price = fields.Numeric("Unit Price", digits=(16, 6))
padding_amount = fields.Numeric("Padding Amount", digits=(16, 2))
provisional_invoice = fields.Many2One(
'account.invoice', "Provisional Invoice")
provisional_date = fields.Date("Provisional Date")
provisional_state = fields.Selection([
('draft', "Draft"),
('validated', "Validated"),
('posted', "Posted"),
('paid', "Paid"),
('cancelled', "Cancelled"),
], "Provisional State")
final_invoice = fields.Many2One('account.invoice', "Final Invoice")
final_date = fields.Date("Final Date")
final_state = fields.Selection([
(None, ''),
('draft', "Draft"),
('validated', "Validated"),
('posted', "Posted"),
('paid', "Paid"),
('cancelled', "Cancelled"),
], "Final State")
padding_status = fields.Selection([
('active', "Active"),
('reversed', "Reversed"),
], "Padding Status")
@classmethod
def table_query(cls):
pool = Pool()
Lot = pool.get('lot.lot')
SaleLine = pool.get('sale.line')
Sale = pool.get('sale.sale')
InvoiceLine = pool.get('account.invoice.line')
Invoice = pool.get('account.invoice')
lot = Lot.__table__()
sale_line = SaleLine.__table__()
sale = Sale.__table__()
prov_line = InvoiceLine.__table__()
prov_invoice = Invoice.__table__()
final_line = InvoiceLine.__table__()
final_invoice = Invoice.__table__()
context = Transaction().context
party = context.get('party')
currency = context.get('currency')
status_filter = context.get('status')
from_date = context.get('from_date')
to_date = context.get('to_date')
sale_filter = context.get('sale')
lot_filter = context.get('lot')
reversed_condition = (
(final_invoice.id > 0)
& ~final_invoice.state.in_(['draft', 'cancelled']))
padding_status = Case(
(reversed_condition, 'reversed'),
else_='active')
where = Literal(True)
where &= lot.sale_invoice_padding > 0
where &= prov_invoice.id > 0
if party:
where &= prov_invoice.party == party
if currency:
where &= prov_invoice.currency == currency
if status_filter and status_filter != 'all':
where &= padding_status == status_filter
if from_date:
where &= prov_invoice.invoice_date >= from_date
if to_date:
where &= prov_invoice.invoice_date <= to_date
if sale_filter:
where &= sale.id == sale_filter
if lot_filter:
where &= lot.id == lot_filter
return (
lot
.join(sale_line, 'LEFT', condition=lot.sale_line == sale_line.id)
.join(sale, 'LEFT', condition=sale_line.sale == sale.id)
.join(prov_line, 'LEFT',
condition=lot.sale_invoice_line_prov == prov_line.id)
.join(prov_invoice, 'LEFT',
condition=prov_line.invoice == prov_invoice.id)
.join(final_line, 'LEFT',
condition=lot.sale_invoice_line == final_line.id)
.join(final_invoice, 'LEFT',
condition=final_line.invoice == final_invoice.id)
.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(0).as_('write_uid'),
Literal(None).as_('write_date'),
lot.id.as_('id'),
prov_invoice.party.as_('party'),
sale.id.as_('sale'),
sale_line.id.as_('sale_line'),
lot.id.as_('lot'),
prov_line.product.as_('product'),
prov_invoice.currency.as_('currency'),
prov_line.unit.as_('unit'),
lot.sale_invoice_padding.as_('padding_quantity'),
prov_line.unit_price.as_('unit_price'),
(lot.sale_invoice_padding * prov_line.unit_price).as_(
'padding_amount'),
prov_invoice.id.as_('provisional_invoice'),
prov_invoice.invoice_date.as_('provisional_date'),
prov_invoice.state.as_('provisional_state'),
final_invoice.id.as_('final_invoice'),
final_invoice.invoice_date.as_('final_date'),
final_invoice.state.as_('final_state'),
padding_status.as_('padding_status'),
where=where,
))
class InvoicePaddingContext(ModelView):
"Invoice with padding context"
__name__ = 'invoice.padding.context'
from_date = fields.Date("From")
to_date = fields.Date("To")
party = fields.Many2One('party.party', "Party")
currency = fields.Many2One('currency.currency', "Currency")
status = fields.Selection([
('all', "All"),
('active', "Active"),
('reversed', "Reversed"),
], "Status")
sale = fields.Many2One('sale.sale', "Sale")
lot = fields.Many2One('lot.lot', "Lot")
@classmethod
def default_from_date(cls):
Date = Pool().get('ir.date')
return Date.today().replace(day=1, month=1, year=1999)
@classmethod
def default_to_date(cls):
Date = Pool().get('ir.date')
return Date.today()
@classmethod
def default_status(cls):
return 'active'
class InvoiceLine(metaclass=PoolMeta):
__name__ = 'account.invoice.line'
def _get_report_trade(self):
origin = getattr(self, 'origin', None)
if not origin:
return None
return getattr(origin, 'sale', None) or getattr(origin, 'purchase', None)
def _get_report_trade_line(self):
return getattr(self, 'origin', None)
@property
def report_product_description(self):
if self.product:
return self.product.description or ''
origin = getattr(self, 'origin', None)
if origin and getattr(origin, 'product', None):
return origin.product.description or ''
return ''
@property
def report_product_name(self):
if self.product:
return self.product.name or ''
origin = getattr(self, 'origin', None)
if origin and getattr(origin, 'product', None):
return origin.product.name or ''
return ''
@property
def report_description_upper(self):
return Invoice._clean_report_description(self.description)
@property
def report_rate_currency_upper(self):
origin = self._get_report_trade_line()
currency = getattr(origin, 'linked_currency', None) or self.currency
if currency and currency.rec_name:
return currency.rec_name.upper()
return ''
@property
def report_rate_value(self):
origin = self._get_report_trade_line()
if origin and getattr(origin, 'price_type', None) == 'basis':
if getattr(origin, 'enable_linked_currency', False) and getattr(origin, 'linked_currency', None):
return Decimal(str(origin.premium or 0))
return Decimal(str(origin._get_premium_price() or 0))
return self.unit_price if self.unit_price is not None else ''
@property
def report_rate_unit_upper(self):
origin = self._get_report_trade_line()
unit = getattr(origin, 'linked_unit', None) or self.unit
if unit and unit.rec_name:
return unit.rec_name.upper()
return ''
@property
def report_rate_price_words(self):
origin = self._get_report_trade_line()
if origin and getattr(origin, 'price_type', None) == 'basis':
value = self.report_rate_value
if self.report_rate_currency_upper == 'USC':
return amount_to_currency_words(value, 'USC', 'USC')
return amount_to_currency_words(value)
trade = self._get_report_trade()
if trade and getattr(trade, 'report_price', None):
return trade.report_price
return ''
@property
def report_rate_pricing_text(self):
origin = self._get_report_trade_line()
return getattr(origin, 'get_pricing_text', '') or ''
@property
def report_crop_name(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'crop', None):
return trade.crop.name or ''
return ''
@property
def report_attributes_name(self):
origin = getattr(self, 'origin', None)
if origin:
return getattr(origin, 'attributes_name', '') or ''
return ''
@property
def report_net(self):
if self.type == 'line':
invoice = getattr(self, 'invoice', None)
if invoice and invoice._report_invoice_line_reuses_lot(self):
return Invoice._get_report_invoice_line_quantity_from_line(self)
lot = getattr(self, 'lot', None)
if lot:
net, _ = Invoice._get_report_lot_hist_weights(lot)
if net is None:
net = 0
sign = Invoice._get_report_line_sign(self)
return Decimal(str(net or 0)) * sign
return self.quantity
return ''
@property
def report_lbs(self):
net = self.report_net
if net == '':
return ''
unit = Invoice._get_report_invoice_line_unit(self)
return Invoice._convert_report_quantity_to_lbs(net, unit)
class ReportTemplateMixin:
@classmethod
def _get_purchase_trade_configuration(cls):
Configuration = Pool().get('purchase_trade.configuration')
configurations = Configuration.search([], limit=1)
return configurations[0] if configurations else None
@classmethod
def _get_action_name(cls, action):
if isinstance(action, dict):
return action.get('name') or ''
return getattr(action, 'name', '') or ''
@classmethod
def _get_action_report_path(cls, action):
if isinstance(action, dict):
return action.get('report') or ''
return getattr(action, 'report', '') or ''
@classmethod
def _resolve_template_path(cls, action, field_name, default_prefix):
config = cls._get_purchase_trade_configuration()
template = getattr(config, field_name, '') if config else ''
template = (template or '').strip()
if not template:
raise UserError('No template found')
if '/' not in template:
return f'{default_prefix}/{template}'
return template
@classmethod
def _get_resolved_action(cls, action):
report_path = cls._resolve_configured_report_path(action)
if isinstance(action, dict):
resolved = dict(action)
resolved['report'] = report_path
return resolved
setattr(action, 'report', report_path)
return action
@classmethod
def _execute(cls, records, header, data, action):
resolved_action = cls._get_resolved_action(action)
return super()._execute(records, header, data, resolved_action)
class InvoiceReport(ReportTemplateMixin, BaseInvoiceReport):
__name__ = 'account.invoice'
@classmethod
def _resolve_configured_report_path(cls, action):
report_path = cls._get_action_report_path(action) or ''
action_name = cls._get_action_name(action)
if (report_path.endswith('/prepayment.fodt')
or action_name == 'Prepayment'):
field_name = 'invoice_prepayment_report_template'
elif report_path.endswith('/packing_list.fodt'):
field_name = 'invoice_packing_list_report_template'
elif (report_path.endswith('/payment_order.fodt')
or action_name == 'Payment Order'):
field_name = 'invoice_payment_order_report_template'
elif (report_path.endswith('/invoice_ict_final.fodt')
or action_name == 'CN/DN'):
field_name = 'invoice_cndn_report_template'
else:
field_name = 'invoice_report_template'
return cls._resolve_template_path(action, field_name, 'account_invoice')
class SaleReport(ReportTemplateMixin, BaseSaleReport):
__name__ = 'sale.sale'
@classmethod
def _resolve_configured_report_path(cls, action):
report_path = cls._get_action_report_path(action)
action_name = cls._get_action_name(action)
if report_path.endswith('/bill.fodt') or action_name == 'Bill':
field_name = 'sale_bill_report_template'
elif report_path.endswith('/sale_final.fodt') or action_name == 'Sale (final)':
field_name = 'sale_final_report_template'
else:
field_name = 'sale_report_template'
return cls._resolve_template_path(action, field_name, 'sale')
class PurchaseReport(ReportTemplateMixin, BasePurchaseReport):
__name__ = 'purchase.purchase'
@classmethod
def _resolve_configured_report_path(cls, action):
return cls._resolve_template_path(
action, 'purchase_report_template', 'purchase')