# tryton_helpers.py — Docstrings

Generated: 2026-02-05 — Summary of Google-style docstrings for helper functions.

## find_party_by_name

def find_party_by_name(party_name)

    Find a party (typically a supplier, client, service provider...) by its name.

    Attempts an exact name match first. If no exact match is found, performs a
    case-insensitive 'ilike' lookup. If still not found, a warning is printed
    and the function returns None.

    Args:
        party_name (str): The name of the party to search for. Leading/trailing
            whitespace is ignored and empty values return None.

    Returns:
        object or None: The first matching Tryton `party.party` record if found,
        otherwise `None` when no match exists or the input is invalid.

    Notes:
        - If multiple matches exist the first record returned by Tryton is used.
        - This function prints a warning when no party is found.

## find_product_by_code

def find_product_by_code(product_code)

    Find a product by its code.

    Attempts an exact code match first. If no exact match is found, performs a
    case-insensitive 'ilike' lookup. If still not found, a warning is printed
    and the function returns None.

    Args:
        product_code (str): The code of the product to search for. Leading and
            trailing whitespace is ignored and empty values return None.

    Returns:
        object or None: The first matching Tryton `product.product` record if
        found; otherwise `None` when no match exists or the input is invalid.

    Notes:
        - If multiple matches exist the first record returned by Tryton is used.
        - This function prints a warning when no product is found.

## find_currency_by_code

def find_currency_by_code(currency_code)

    Find a currency by its ISO code or name.

    Performs a case-sensitive search by currency name after uppercasing the
    input. Returns the first matching `currency.currency` record. If the input
    is empty or no match is found, a warning is printed and the function
    returns `None`.

    Args:
        currency_code (str): The currency code or name to search for. Leading
            and trailing whitespace is ignored; empty values return `None`.

    Returns:
        object or None: The first matching Tryton `currency.currency` record
        if found; otherwise `None` when no match exists or the input is
        invalid.

    Notes:
        - This function searches by the `name` field using an uppercased exact
          match. Consider expanding to include ISO code or `ilike` searches if
          needed in the future.
        - If multiple matches exist the first record returned by Tryton is used.

## find_contract_line_by_sequence

def find_contract_line_by_sequence(contract, line_sequence)

    Find a contract line within a purchase contract by its sequence number.

    Validates the provided contract and attempts to convert the provided
    `line_sequence` to an integer. Searches the `lines` iterable on the
    contract for a line object whose `sequence` attribute matches the
    integer sequence number. Returns the first matching line, or `None` if the
    contract is invalid, the sequence cannot be parsed, or no matching line is
    found.

    Args:
        contract (object): A `purchase.purchase` record (or similar) expected
            to have a `lines` iterable attribute containing line objects.
        line_sequence (int | str): Sequence number to search for. Can be an
            integer or a string representation of an integer.

    Returns:
        object or None: The matching contract line object if found; otherwise
        `None`.

    Notes:
        - Prints a warning when the provided `line_sequence` is invalid or when
          no matching line is found.

## find_purchase_contract_by_ref

def find_purchase_contract_by_ref(contract_ref)

    Find a purchase contract by its reference identifier.

    Performs an exact match lookup on the `reference` field of the
    `purchase.purchase` model. If the input is empty or no contract is found,
    the function prints a warning and returns `None`.

    Args:
        contract_ref (str): The reference string of the purchase contract.
            Leading/trailing whitespace is ignored and empty values return None.

    Returns:
        object or None: The first matching Tryton `purchase.purchase` record if
        found; otherwise `None` when no match exists or the input is invalid.

    Notes:
        - If multiple matches exist the first record returned by Tryton is used.
        - This function prints a warning when no contract is found.

## find_supplier_category

def find_supplier_category()

    Retrieve the 'SUPPLIER' party category from the system.

    First attempts an exact match on the `name` field for 'SUPPLIER'. If an
    exact match is not found, the function falls back to iterating all party
    categories and returns the first one whose uppercased `name` equals
    'SUPPLIER'. If no matching category is found, a warning is printed and
    `None` is returned.

    Args:
        None

    Returns:
        object or None: The matching `party.category` record if found; otherwise
        `None`.

    Notes:
        - This helper helps ensure that parties can be categorized as suppliers
          without relying on exact case-sensitive persistence of the category
          name.

## ensure_party_is_supplier

def ensure_party_is_supplier(party, auto_enable=True)

    Ensure a party has the SUPPLIER category, optionally adding it.

    Checks whether the provided `party` record contains the SUPPLIER
    category. If the category is missing and `auto_enable` is True, the
    function attempts to append the category to the party and save the
    record. On success it returns the updated party and True. If
    `auto_enable` is False the function leaves the party unchanged and
    returns (party, False), printing guidance for manual action.

    Args:
        party (object): A `party.party` record expected to have a
            `categories` collection attribute.
        auto_enable (bool): If True (default) attempt to add the SUPPLIER
            category when missing; if False do not modify the party and
            prompt the user to add the category manually.

    Returns:
        tuple: (party, bool) where bool is True if the party has the
        SUPPLIER category after the call, otherwise False.

    Notes:
        - Prints informative messages for missing category, permission
          issues, and other exceptions. Use `find_supplier_category()` to
          retrieve the category object directly if needed.

## find_fee_mode_by_name

def find_fee_mode_by_name(mode_name)

    Map a human-readable fee mode name to the system's internal mode code.

    Normalizes the input (trims whitespace and uppercases) and returns a
    short code used internally. Known mappings are:
      - 'PER QT' -> 'perqt'
      - '% COST PRICE' -> 'pcost'
      - '% PRICE' -> 'pprice'
      - 'LUMP SUM' -> 'lumpsum'

    Args:
        mode_name (str): Fee mode display name. Leading/trailing whitespace is
            ignored and comparison is case-insensitive.

    Returns:
        str or None: The mapped internal mode string if recognized, otherwise
        `None` for unknown or empty inputs.

    Notes:
        - Prints a warning when an unknown mode is encountered.

## find_payable_receivable_by_name

def find_payable_receivable_by_name(p_r_value)

    Determine whether a fee is payable or receivable from a P_R-style value.

    Normalizes the input by trimming whitespace and uppercasing it, then maps
    common variants to either 'pay' or 'rec'. Recognised payable values include
    'PAY', 'PAYABLE', and 'P'; recognised receivable values include 'REC',
    'RECEIVABLE', and 'R'. An empty or falsy input returns `None`. Unknown
    values print a warning and default to 'pay'.

    Args:
        p_r_value (str): Raw value from the P_R column (e.g., 'PAY', 'REC').

    Returns:
        str or None: 'pay' for payable, 'rec' for receivable, or `None` for
        empty/invalid inputs.

    Notes:
        - Prints a warning when encountering an unrecognised value and
          defaults to 'pay' to maintain backward compatibility.

## get_existing_fees_for_line

def get_existing_fees_for_line(contract_line)

    Retrieve the existing fees associated with a contract line.

    Validates the provided `contract_line` and returns its `fees` collection
    if present. If the contract line is missing or does not expose a `fees`
    attribute, the function returns an empty list to simplify downstream
    duplicate checks and iteration.

    Args:
        contract_line (object): A contract line object expected to have a
            `fees` iterable attribute (may be None or empty).

    Returns:
        list: The fees associated with the contract line, or an empty list if
        none exist or the input is invalid.

## fee_already_exists

def fee_already_exists(existing_fees, product, supplier, price)

    Check whether a fee with the same product, supplier and price already exists.

    Iterates `existing_fees` and compares each fee's `product.id`, `party.id`,
    and `price` to the provided `product`, `supplier`, and `price` respectively.
    The function performs attribute presence checks to avoid AttributeError and
    uses exact equality for price comparison.

    Args:
        existing_fees (iterable): Iterable of fee objects (may be a list or
            None). Each fee is expected to expose `product`, `party`, and
            `price` attributes.
        product (object): Product record with an `id` attribute.
        supplier (object): Supplier/party record with an `id` attribute.
        price (Decimal | number): Price value to match against fee.price.

    Returns:
        bool: True if a matching fee exists; False otherwise.

    Notes:
        - Exact equality is used for price comparison; consider tolerances when
          comparing floating point values.

## parse_decimal

def parse_decimal(value, field_name)

    Parse and validate a numeric value into a Decimal.

    Converts `value` to a Decimal using `Decimal(str(value))`. Returns `None`
    for empty inputs, common string null markers (e.g. 'NULL', 'NONE', 'N/A'),
    or when the value cannot be parsed as a decimal (in which case a warning
    is printed referencing `field_name`).

    Args:
        value (str|int|Decimal|None): The raw value to parse into a Decimal.
        field_name (str): Name of the field (used to provide contextual
            information in warning messages).

    Returns:
        Decimal or None: A Decimal instance when parsing succeeds; otherwise
        `None` for empty/invalid inputs.

    Notes:
        - Uses `Decimal(str(value))` to avoid floating-point precision issues.
        - Catching `ValueError` and `TypeError` ensures the function is safe to
          call on arbitrary input values encountered while importing data.