from decimal import Decimal, ROUND_HALF_UP from datetime import date as dt_date import os from pathlib import Path 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.tools import file_open 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 InvoiceLineLotWeight(ModelSQL, ModelView): "Invoice Line Lot Weight" __name__ = 'account.invoice.line.lot.weight' invoice_line = fields.Many2One( 'account.invoice.line', "Invoice Line", required=True, ondelete='CASCADE') lot = fields.Many2One('lot.lot', "Lot", required=True, ondelete='CASCADE') lot_qt_hist = fields.Many2One('lot.qt.hist', "Quantity History") quantity_type = fields.Many2One('lot.qt.type', "Quantity Type") net_quantity = fields.Numeric("Net Weight", digits=(1, 5), required=True) gross_quantity = fields.Numeric( "Gross Weight", digits=(1, 5), required=True) unit = fields.Many2One('product.uom', "Unit") lot_qt = fields.Integer("Packing Quantity") lot_unit = fields.Many2One('product.uom', "Packing Unit") @classmethod def _get_lot_hist_entry(cls, lot): state = getattr(lot, 'lot_state', None) state_id = getattr(state, 'id', None) hist = list(getattr(lot, 'lot_hist', []) or []) 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: return entry if len(hist) == 1: return hist[0] return None @classmethod def _get_lot_weights(cls, lot): hist = cls._get_lot_hist_entry(lot) if hist: net = Decimal(str(getattr(hist, 'quantity', 0) or 0)) gross = Decimal(str( getattr(hist, 'gross_quantity', None) if getattr(hist, 'gross_quantity', None) not in (None, '') else net)) return hist, net, gross if hasattr(lot, 'get_hist_quantity'): net, gross = lot.get_hist_quantity() net = Decimal(str(net or 0)) gross = Decimal(str(gross if gross not in (None, '') else net)) return None, net, gross net = Decimal(str(getattr(lot, 'lot_quantity', 0) or 0)) gross = Decimal(str( getattr(lot, 'lot_gross_quantity', None) if getattr(lot, 'lot_gross_quantity', None) not in (None, '') else net)) return None, net, gross @classmethod def create_for_invoice_line(cls, line, lot): if not getattr(line, 'id', None) or not getattr(lot, 'id', None): return existing = cls.search([ ('invoice_line', '=', line.id), ('lot', '=', lot.id), ]) if existing: cls.delete(existing) hist, net, gross = cls._get_lot_weights(lot) snapshot = cls() snapshot.invoice_line = line snapshot.lot = lot snapshot.lot_qt_hist = hist snapshot.quantity_type = ( getattr(hist, 'quantity_type', None) or getattr(lot, 'lot_state', None)) snapshot.net_quantity = net snapshot.gross_quantity = gross snapshot.unit = ( getattr(lot, 'lot_unit_line', None) or getattr(lot, 'lot_unit', None) or getattr(line, 'unit', None)) snapshot.lot_qt = getattr(lot, 'lot_qt', None) snapshot.lot_unit = getattr(lot, 'lot_unit', None) cls.save([snapshot]) class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' def do_lot_invoicing(self): super().do_lot_invoicing() self._create_lot_weight_snapshots() self._create_sale_padding_moves() def _create_lot_weight_snapshots(self): Snapshot = Pool().get('account.invoice.line.lot.weight') Lot = Pool().get('lot.lot') for line in self._get_report_invoice_lines(): if (getattr(line, 'description', None) != 'Pro forma' or Decimal(str(getattr(line, 'quantity', 0) or 0)) <= 0 or not getattr(line, 'lot', None)): continue Snapshot.create_for_invoice_line(line, Lot(line.lot.id)) @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 _report_record_name(record): if not record: return '' return ( getattr(record, 'rec_name', None) or getattr(record, 'name', None) or getattr(record, 'code', None) or '') def _get_report_commission_fee(self): for line in self._get_report_invoice_lines(): fee = getattr(line, 'fee', None) if fee: return fee origin = getattr(line, 'origin', None) fee = getattr(origin, 'fee_', None) if fee: return fee def _get_report_commission_lots(self): 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) for line in self._get_report_invoice_lines(): add_lot(getattr(line, 'lot', None)) fee = self._get_report_commission_fee() if fee and hasattr(fee, '_get_effective_fee_lots'): for lot in fee._get_effective_fee_lots(): add_lot(lot) for lot in getattr(fee, 'lots', []) or []: add_lot(lot) return lots def _get_report_commission_trade_for(self, kind): fee = self._get_report_commission_fee() if fee: if kind == 'sale' and getattr(fee, 'sale_line', None): return fee.sale_line.sale if kind == 'purchase' and getattr(fee, 'line', None): return fee.line.purchase for lot in self._get_report_commission_lots(): if kind == 'sale' and getattr(lot, 'sale_line', None): return lot.sale_line.sale if kind == 'purchase' and getattr(lot, 'line', None): return lot.line.purchase records = getattr(self, kind + 's', None) if records: return records[0] def _get_report_commission_trade(self): for kind in ('sale', 'purchase'): trade = self._get_report_commission_trade_for(kind) if trade: return trade for line in self._get_report_invoice_lines(): origin = getattr(line, 'origin', None) if not origin: continue trade = getattr(origin, 'sale', None) or getattr(origin, 'purchase', None) if trade: return trade @property def report_commission_address(self): trade = self._get_report_commission_trade() return getattr(trade, 'report_address', '') or '' @property def report_commission_date(self): trade = self._get_report_commission_trade() return ( getattr(trade, 'sale_date', None) or getattr(trade, 'purchase_date', None) or self.invoice_date) def _get_report_commission_value(self, name): trade = self._get_report_commission_trade() return getattr(trade, name, '') if trade else '' def _get_report_commission_value_for(self, kind, name): trade = self._get_report_commission_trade_for(kind) return getattr(trade, name, '') if trade else '' @property def report_commission_invoice_number(self): return self._get_report_commission_value( 'report_commission_invoice_number') @property def report_commission_contract_number(self): return self._get_report_commission_value( 'report_commission_contract_number') @property def report_commission_bl_number(self): return self._get_report_commission_value( 'report_commission_bl_number') @property def report_commission_bl_date(self): return self._get_report_commission_value( 'report_commission_bl_date') @property def report_commission_vessel(self): return self._get_report_commission_value( 'report_commission_vessel') @property def report_commission_invoice_line(self): return self._get_report_commission_value( 'report_commission_invoice_line') @property def report_commission_quantity_unit_upper(self): return self._get_report_commission_value( 'report_commission_quantity_unit_upper') @property def report_commission_quantity_display(self): return self._get_report_commission_value( 'report_commission_quantity_display') @property def report_commission_lbs_display(self): return self._get_report_commission_value( 'report_commission_lbs_display') @property def report_commission_rate_line(self): return self._get_report_commission_value( 'report_commission_rate_line') @property def report_commission_secondary_rate_line(self): return self._get_report_commission_value( 'report_commission_secondary_rate_line') def _get_report_commission_base_amount_display_for(self, kind): trade = self._get_report_commission_trade_for(kind) if trade: base = getattr(trade, 'report_commission_base_amount_display', None) if base: return base amount = ( getattr(trade, 'untaxed_amount', None) or getattr(trade, 'total_amount', None) or 0) return self._format_report_number( amount, digits='0.01', strip_trailing_zeros=False) return self.report_commission_total_display def _get_report_commission_currency_name_for(self, kind): trade = self._get_report_commission_trade_for(kind) currency = getattr(trade, 'currency', None) if trade else self.currency return self._report_record_name(currency) def _get_report_commission_rate_line_for(self, kind): fee = self._get_report_commission_fee() if not fee: return self._get_report_commission_value( 'report_commission_rate_line') mode = getattr(fee, 'mode', None) price = getattr(fee, 'price', None) or 0 if mode in {'pprice', 'rate', 'pcost'}: return ' '.join(part for part in [ self._format_report_number(price, digits='0.0000'), '% on', kind, 'amount', self._get_report_commission_base_amount_display_for(kind), self._get_report_commission_currency_name_for(kind), ] if part) if (getattr(fee, 'enable_linked_currency', False) and getattr(fee, 'linked_price', None) is not None): price = fee.linked_price currency = getattr(fee, 'linked_currency', None) unit = getattr(fee, 'linked_unit', None) else: currency = getattr(fee, 'currency', None) unit = getattr(fee, 'unit', None) price_text = self._format_report_number(price, digits='0.0000') currency_text = self._report_record_name(currency) unit_text = self._report_record_name(unit) if currency_text and unit_text: return '%s %s/%s' % (price_text, currency_text, unit_text) return ' '.join(part for part in [ price_text, currency_text or unit_text, ] if part) @property def report_sale_commission_rate_line(self): return self._get_report_commission_rate_line_for('sale') @property def report_purchase_commission_rate_line(self): return self._get_report_commission_rate_line_for('purchase') @property def report_sale_commission_secondary_rate_line(self): return '' @property def report_purchase_commission_secondary_rate_line(self): return '' @property def report_commission_currency_name(self): return self._get_report_commission_value( 'report_commission_currency_name') @property def report_commission_total_display(self): return self._get_report_commission_value( 'report_commission_total_display') def _get_report_commission_address_for(self, kind): trade = self._get_report_commission_trade_for(kind) return getattr(trade, 'report_address', '') or '' def _get_report_commission_date_for(self, kind): trade = self._get_report_commission_trade_for(kind) return ( getattr(trade, 'sale_date', None) or getattr(trade, 'purchase_date', None) or self.invoice_date) def _get_report_commission_total_display_for(self, kind): fee = self._get_report_commission_fee() if fee and hasattr(fee, 'get_amount'): amount = fee.get_amount() else: amount = self.total_amount or 0 return self._format_report_number( abs(Decimal(str(amount or 0))), digits='0.01', strip_trailing_zeros=False) @property def report_sale_commission_address(self): return self._get_report_commission_address_for('sale') @property def report_purchase_commission_address(self): return self._get_report_commission_address_for('purchase') @property def report_sale_commission_date(self): return self._get_report_commission_date_for('sale') @property def report_purchase_commission_date(self): return self._get_report_commission_date_for('purchase') @property def report_sale_commission_invoice_number(self): return self._get_report_commission_value_for( 'sale', 'report_commission_invoice_number') @property def report_purchase_commission_invoice_number(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_invoice_number') @property def report_sale_commission_contract_number(self): return self._get_report_commission_value_for( 'sale', 'report_commission_contract_number') @property def report_purchase_commission_contract_number(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_contract_number') @property def report_sale_commission_bl_number(self): return self._get_report_commission_value_for( 'sale', 'report_commission_bl_number') @property def report_purchase_commission_bl_number(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_bl_number') @property def report_sale_commission_bl_date(self): return self._get_report_commission_value_for( 'sale', 'report_commission_bl_date') @property def report_purchase_commission_bl_date(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_bl_date') @property def report_sale_commission_vessel(self): return self._get_report_commission_value_for( 'sale', 'report_commission_vessel') @property def report_purchase_commission_vessel(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_vessel') @property def report_sale_commission_invoice_line(self): return self._get_report_commission_value_for( 'sale', 'report_commission_invoice_line') @property def report_purchase_commission_invoice_line(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_invoice_line') @property def report_sale_commission_quantity_unit_upper(self): return self._get_report_commission_value_for( 'sale', 'report_commission_quantity_unit_upper') @property def report_purchase_commission_quantity_unit_upper(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_quantity_unit_upper') @property def report_sale_commission_quantity_display(self): return self._get_report_commission_value_for( 'sale', 'report_commission_quantity_display') @property def report_purchase_commission_quantity_display(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_quantity_display') @property def report_sale_commission_lbs_display(self): return self._get_report_commission_value_for( 'sale', 'report_commission_lbs_display') @property def report_purchase_commission_lbs_display(self): return self._get_report_commission_value_for( 'purchase', 'report_commission_lbs_display') @property def report_sale_commission_currency_name(self): return self._report_record_name(self.currency) @property def report_purchase_commission_currency_name(self): return self._report_record_name(self.currency) @property def report_sale_commission_total_display(self): return self._get_report_commission_total_display_for('sale') @property def report_purchase_commission_total_display(self): return self._get_report_commission_total_display_for('purchase') @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): snapshots = self._get_report_invoice_line_weight_snapshots(line) if snapshots: sign = self._get_report_line_sign(line) net_total = sum( Decimal(str(snapshot.net_quantity or 0)) for snapshot in snapshots) gross_total = sum( Decimal(str(snapshot.gross_quantity or 0)) for snapshot in snapshots) unit = self._get_report_invoice_line_unit(line) padding = self._get_report_invoice_line_padding(line, unit) net_total += padding gross_total += padding return net_total * sign, gross_total * sign 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_invoice_line_weight_snapshots(line): snapshots = getattr(line, 'lot_weight_snapshots', None) if snapshots and isinstance(snapshots, (list, tuple)): return list(snapshots) line_id = getattr(line, 'id', None) if not isinstance(line_id, int): return [] Snapshot = Pool().get('account.invoice.line.lot.weight') return Snapshot.search([('invoice_line', '=', line_id)]) @classmethod def _get_report_invoice_line_padding(cls, line, to_unit): if getattr(line, 'description', None) != 'Pro forma': return Decimal(0) invoice_type = getattr(line, 'invoice_type', None) invoice = getattr(line, 'invoice', None) if invoice_type != 'out' and getattr(invoice, 'type', None) != 'out': return Decimal(0) lot = getattr(line, 'lot', None) padding = Decimal(str( getattr(lot, 'sale_invoice_padding', 0) or 0)) if not padding: return Decimal(0) return cls._convert_report_quantity( padding, getattr(line, 'unit', None), to_unit) @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 def _get_report_weight_adjustment_totals(self): reused_lots = self._get_report_reused_lot_lines() if not reused_lots: return None invoice_total = Decimal(0) landed_total = Decimal(0) matched = False for data in reused_lots.values(): lot = data['lot'] lines = data['lines'] negative_lines = [ line for line in lines if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0 ] positive_lines = [ line for line in lines if Decimal(str(getattr(line, 'quantity', 0) or 0)) > 0 ] if not negative_lines or not positive_lines: continue invoice_total += sum( abs(self._get_report_invoice_line_weights(line)[0]) for line in negative_lines) current_net, _ = self._get_report_lot_hist_weights(lot) if current_net is None: landed_total += sum( abs(self._get_report_invoice_line_weights(line)[0]) for line in positive_lines) else: landed_total += current_net matched = True if not matched: return None return { 'invoice': invoice_total, 'landed': landed_total, 'difference': abs(landed_total - invoice_total), } @staticmethod def _get_report_invoice_line_unit(line): snapshots = Invoice._get_report_invoice_line_weight_snapshots(line) if snapshots and getattr(snapshots[0], 'unit', None): return snapshots[0].unit 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 _get_report_bank_address(account): bank = getattr(account, 'bank', None) if account else None party = getattr(bank, 'party', None) if bank else None address_get = getattr(party, 'address_get', None) if party else None if callable(address_get): return address_get() return None @staticmethod def _get_report_bank_party_name(account): bank = getattr(account, 'bank', None) if account else None party = getattr(bank, 'party', None) if bank else None address = Invoice._get_report_bank_address(account) return ( getattr(address, 'party_full_name', None) or getattr(address, 'party_name', None) or getattr(party, 'rec_name', None) or getattr(bank, 'rec_name', None) or '') @staticmethod def _get_report_bank_address_lines(account): address = Invoice._get_report_bank_address(account) if not address: return '' city_line = ' '.join(filter(None, [ getattr(address, 'postal_code', None), getattr(address, 'city', None), ])) full_address = '\n'.join(filter(None, [ getattr(address, 'name', None), getattr(address, 'street', None), city_line, ])) return '\n'.join( line for line in (full_address or '').splitlines() if line.strip()) @staticmethod def _format_report_payment_amount(value): amount = Decimal(str(value or 0)).quantize(Decimal('0.01')) return format(amount, 'f') def _get_report_melya_bank_account(self): sale = self._get_report_deal_sale() return getattr(sale, 'our_bank_account', None) if sale else None @property def report_melya_bank_name(self): return self._get_report_bank_party_name( self._get_report_melya_bank_account()) @property def report_melya_bank_address(self): return self._get_report_bank_address_lines( self._get_report_melya_bank_account()) @property def report_melya_bank_iban(self): return self._get_report_bank_account_number( self._get_report_melya_bank_account()) @property def report_melya_bank_swift(self): return self._get_report_bank_swift( self._get_report_melya_bank_account()) @property def report_melya_bank_details_line(self): name = self.report_melya_bank_name return 'BANK DETAILS:\t\t%s' % name if name else '' @property def report_melya_bank_account_line(self): parts = [] iban = self.report_melya_bank_iban swift = self.report_melya_bank_swift if iban: parts.append('IBAN : %s' % iban) if swift: parts.append('Swift Code: %s' % swift) return ', '.join(parts) @property def report_melya_maturity_date(self): maturity_dates = [ line.maturity_date for line in (self.lines_to_pay or []) if getattr(line, 'maturity_date', None)] if maturity_dates: return min(maturity_dates) @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()] @property def report_melya_rate_rows(self): return self._get_report_price_composition_rows() 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_packaging(self): nb_bale = self.report_nb_bale if nb_bale.startswith('NB ') and ': ' in nb_bale: label, quantity = nb_bale[3:].split(': ', 1) return f"{quantity} {label}" 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_lbs_display(self): lbs = self.report_gross_lbs if lbs == '': return '' return self._format_report_quantity_display(lbs) @property def report_weight_difference_display(self): difference = self.report_weight_difference if difference == '': return '' return self._format_report_quantity_display(difference) @property def report_weight_difference_lbs_display(self): lbs = self.report_weight_difference_lbs if lbs == '': return '' return self._format_report_quantity_display(lbs) @property def report_gross(self): if self.lines: adjustment = self._get_report_weight_adjustment_totals() if adjustment: return adjustment['landed'] 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: adjustment = self._get_report_weight_adjustment_totals() if adjustment: return adjustment['invoice'] 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_gross_lbs(self): gross = self.report_gross if gross == '': 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(gross, unit) @property def report_weight_difference(self): adjustment = self._get_report_weight_adjustment_totals() if adjustment: return adjustment['difference'] gross = self.report_gross net = self.report_net if gross == '' or net == '': return '' return gross - net @property def report_weight_difference_lbs(self): difference = self.report_weight_difference if difference == '': 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(difference, 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 '' if getattr(shipment, 'transport_type', None) == 'truck': return 'By Truck' vessel = getattr(shipment, 'vessel', None) vessel_name = getattr(vessel, 'vessel_name', None) or '' return vessel_name @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) snapshots = Invoice._get_report_invoice_line_weight_snapshots(self) if snapshots: sign = Invoice._get_report_line_sign(self) net = sum( Decimal(str(snapshot.net_quantity or 0)) for snapshot in snapshots) unit = Invoice._get_report_invoice_line_unit(self) net += Invoice._get_report_invoice_line_padding(self, unit) return net * sign 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 _load_report_content(cls, report_path): file_name = report_path.replace('/', os.sep) try: with file_open(file_name, mode='rb') as fp: return fp.read() except FileNotFoundError: modules_dir = Path(__file__).resolve().parent.parent path = modules_dir.joinpath(*report_path.split('/')) try: return path.read_bytes() except FileNotFoundError: raise UserError('No template found') @classmethod def _get_resolved_action(cls, action): report_path = cls._resolve_configured_report_path(action) report_content = cls._load_report_content(report_path) if isinstance(action, dict): resolved = dict(action) resolved['report'] = report_path resolved['report_content'] = report_content return resolved setattr(action, 'report', report_path) setattr(action, 'report_content', report_content) action._template_cache.clear() 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) legacy_prefix = None 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' elif (report_path.endswith('/commission__ict_final.fodt') or action_name == 'CN/DN Commission'): field_name = 'invoice_commission_cndn_report_template' elif (report_path.endswith('/sale_commission_ict.fodt') or action_name == 'Commission invoice Sale'): field_name = 'sale_commission_report_template' legacy_prefix = 'sale/' elif (report_path.endswith('/purchase_commission_ict.fodt') or action_name == 'Commission invoice Purchase'): field_name = 'purchase_commission_report_template' legacy_prefix = 'purchase/' else: field_name = 'invoice_report_template' resolved_path = cls._resolve_template_path( action, field_name, 'account_invoice') if legacy_prefix and resolved_path.startswith(legacy_prefix): return 'account_invoice/%s' % resolved_path.rsplit('/', 1)[-1] return resolved_path 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): report_path = cls._get_action_report_path(action) action_name = cls._get_action_name(action) return cls._resolve_template_path( action, 'purchase_report_template', 'purchase')