Skip to content

biip.rcn

Restricted Circulation Number (RCN) is a subset of GTIN.

Both RCN-8, RCN-12, and RCN-13 are supported. There is no 14 digit version of RCN.

RCN-12 with prefix 2 and RCN-13 with prefix 02 or 20-29 have the same semantics across a geographic region, defined by the local GS1 Member Organization.

RCN-8 with prefix 0 or 2, RCN-12 with prefix 4, and RCN-13 with prefix 04 or 40-49 have semantics that are only defined within a single company.

Use Gtin.parse() to parse potential RCNs. This subclass is returned if the GS1 Prefix signifies that the value is an RCN.

References

GS1 General Specifications, section 2.1.11-2.1.12

Rcn dataclass

Bases: Gtin

Data class containing an RCN.

This is a subclass of Gtin. To create an Rcn instance, use Gtin.parse() with an RCN string as the value to parse.

Source code in src/biip/rcn.py
@dataclass(frozen=True)
class Rcn(Gtin):
    """Data class containing an RCN.

    This is a subclass of [`Gtin`][biip.gtin.Gtin]. To create an `Rcn` instance,
    use [`Gtin.parse()`][biip.gtin.Gtin.parse] with an RCN string as the value
    to parse.
    """

    usage: RcnUsage | None = field(default=None)
    """Where the RCN can be circulated, in a geographical region or within a company."""

    region: RcnRegion | None = field(default=None)
    """The geographical region.

    The region's rules are used to interpret the contents of the RCN.
    """

    weight: Decimal | None = field(default=None)
    """A variable weight value extracted from the GTIN."""

    count: int | None = field(default=None)
    """A variable count extracted from the GTIN."""

    price: Decimal | None = field(default=None)
    """A variable weight price extracted from the GTIN."""

    money: moneyed.Money | None = field(default=None)
    """A Money value created from the variable weight price.

    Only set if [`py-moneyed`](https://pypi.org/project/py-moneyed/) is
    installed and the currency is known.
    """

    def __rich_repr__(self) -> Iterator[tuple[str, Any] | tuple[str, Any, Any]]:  # noqa: D105
        # Skip printing fields with default values
        yield from super().__rich_repr__()
        yield "usage", self.usage, None
        yield "region", self.region, None
        yield "weight", self.weight, None
        yield "count", self.count, None
        yield "price", self.price, None
        yield "money", self.money, None

    def _with_usage(self) -> Rcn:
        # Classification as RCN depends on the prefix being known, so we won't
        # get here unless it is known.
        assert self.prefix is not None

        usage = None
        if "within a geographic region" in self.prefix.usage:
            usage = RcnUsage.GEOGRAPHICAL
        if "within a company" in self.prefix.usage:
            usage = RcnUsage.COMPANY

        return replace(self, usage=usage)

    def _parsed_with_regional_rules(self, *, config: ParseConfig) -> Rcn:
        rcn = self

        if rcn.usage == RcnUsage.COMPANY:
            # The value is an RCN, but it is intended for use within a company,
            # so we can only interpret it as an opaque GTIN.
            return rcn

        if config.rcn_region is None:
            # The region is not known, so we cannot interpret the variable measure.
            return rcn

        rcn = replace(rcn, region=RcnRegion(config.rcn_region))

        strategy = _Strategy.get_for_rcn(rcn)
        if strategy is None:
            # Without a strategy, we cannot extract anything.
            return rcn

        if config.rcn_verify_variable_measure:
            strategy.verify_check_digit(rcn)

        if strategy.measure_type == _MeasureType.WEIGHT:
            weight = strategy.get_variable_measure(self)
        else:
            weight = None

        if strategy.measure_type == _MeasureType.COUNT:
            count = int(strategy.get_variable_measure(self))
        else:
            count = None

        if strategy.measure_type == _MeasureType.PRICE:
            price = strategy.get_variable_measure(self)
        else:
            price = None

        assert rcn.region
        currency_code = rcn.region.get_currency_code()
        if price is not None and have_moneyed and currency_code is not None:
            import moneyed

            money = moneyed.Money(amount=price, currency=currency_code)
        else:
            money = None

        return replace(
            rcn,
            weight=weight,
            count=count,
            price=price,
            money=money,
        )

    def without_variable_measure(self) -> Gtin:
        """Create a new RCN where the variable measure is zeroed out.

        This provides us with a number which still includes the item
        reference, but does not vary with weight/price, and can thus be used
        to lookup the relevant trade item in a database or similar.

        This has no effect on RCNs intended for use within a company, as
        the semantics of those numbers vary from company to company.

        Returns:
            A RCN instance with zeros in the variable measure places.

        Raises:
            EncodeError: If the rules for variable measures in the region are unknown.
        """
        if self.usage == RcnUsage.COMPANY:
            # The value is an RCN, but it is intended for use within a company,
            # so we can only interpret it as an opaque GTIN.
            return self

        if self.region is None:
            msg = (
                f"Cannot zero out the variable measure part of {self.value!r} as the "
                f"RCN rules for the geographical region {self.region!r} are unknown."
            )
            raise EncodeError(msg)

        strategy = _Strategy.get_for_rcn(self)
        if strategy is None:
            # This prefix has no rules for removing variable parts.
            return self

        return strategy.without_variable_measure(self)

check_digit instance-attribute

check_digit: int

Check digit used to check if the GTIN as a whole is valid.

company_prefix instance-attribute

company_prefix: GS1CompanyPrefix | None

The GS1 Company Prefix.

Identifying the company that issued the GTIN.

count class-attribute instance-attribute

count: int | None = field(default=None)

A variable count extracted from the GTIN.

format instance-attribute

format: GtinFormat

GTIN format, either GTIN-8, GTIN-12, GTIN-13, or GTIN-14.

Classification is done after stripping leading zeros.

money class-attribute instance-attribute

money: Money | None = field(default=None)

A Money value created from the variable weight price.

Only set if py-moneyed is installed and the currency is known.

packaging_level class-attribute instance-attribute

packaging_level: int | None = None

Packaging level is the first digit in GTIN-14 codes.

This digit is used for wholesale shipments, e.g. the GTIN-14 product identifier in GS1-128 barcodes, but not in the GTIN-13 barcodes used for retail products.

payload instance-attribute

payload: str

The actual payload.

Including packaging level if any, company prefix, and item reference. Excludes the check digit.

prefix instance-attribute

prefix: GS1Prefix | None

The GS1 Prefix.

Indicating what GS1 country organization that assigned code range.

price class-attribute instance-attribute

price: Decimal | None = field(default=None)

A variable weight price extracted from the GTIN.

region class-attribute instance-attribute

region: RcnRegion | None = field(default=None)

The geographical region.

The region's rules are used to interpret the contents of the RCN.

usage class-attribute instance-attribute

usage: RcnUsage | None = field(default=None)

Where the RCN can be circulated, in a geographical region or within a company.

value instance-attribute

value: str

Raw unprocessed value.

May include leading zeros.

weight class-attribute instance-attribute

weight: Decimal | None = field(default=None)

A variable weight value extracted from the GTIN.

as_gtin_12

as_gtin_12() -> str

Format as a GTIN-12.

Source code in src/biip/gtin.py
def as_gtin_12(self) -> str:
    """Format as a GTIN-12."""
    return self._as_format(GtinFormat.GTIN_12)

as_gtin_13

as_gtin_13() -> str

Format as a GTIN-13.

Source code in src/biip/gtin.py
def as_gtin_13(self) -> str:
    """Format as a GTIN-13."""
    return self._as_format(GtinFormat.GTIN_13)

as_gtin_14

as_gtin_14() -> str

Format as a GTIN-14.

Source code in src/biip/gtin.py
def as_gtin_14(self) -> str:
    """Format as a GTIN-14."""
    return self._as_format(GtinFormat.GTIN_14)

as_gtin_8

as_gtin_8() -> str

Format as a GTIN-8.

Source code in src/biip/gtin.py
def as_gtin_8(self) -> str:
    """Format as a GTIN-8."""
    return self._as_format(GtinFormat.GTIN_8)

parse classmethod

parse(
    value: str, *, config: ParseConfig | None = None
) -> Gtin

Parse the given value into a Gtin object.

Both GTIN-8, GTIN-12, GTIN-13, and GTIN-14 are supported.

The checksum is guaranteed to be valid if a GTIN object is returned.

Parameters:

  • value (str) –

    The value to parse.

  • config (ParseConfig | None, default: None ) –

    Configuration options for parsing.

Returns:

  • Gtin

    GTIN data structure with the successfully extracted data.

Raises:

Source code in src/biip/gtin.py
@classmethod
def parse(
    cls,
    value: str,
    *,
    config: ParseConfig | None = None,
) -> Gtin:
    """Parse the given value into a [`Gtin`][biip.gtin.Gtin] object.

    Both GTIN-8, GTIN-12, GTIN-13, and GTIN-14 are supported.

    The checksum is guaranteed to be valid if a GTIN object is returned.

    Args:
        value: The value to parse.
        config: Configuration options for parsing.

    Returns:
        GTIN data structure with the successfully extracted data.

    Raises:
        ParseError: If the parsing fails.
    """
    if config is None:
        config = ParseConfig()

    from biip.rcn import Rcn

    value = value.strip()

    if len(value) not in (8, 12, 13, 14):
        msg = (
            f"Failed to parse {value!r} as GTIN: "
            f"Expected 8, 12, 13, or 14 digits, got {len(value)}."
        )
        raise ParseError(msg)

    if not value.isdecimal():
        msg = f"Failed to parse {value!r} as GTIN: Expected a numerical value."
        raise ParseError(msg)

    stripped_value = _strip_leading_zeros(value)
    assert len(stripped_value) in (8, 12, 13, 14)

    num_significant_digits = len(stripped_value)
    gtin_format = GtinFormat(num_significant_digits)

    payload = stripped_value[:-1]
    check_digit = int(stripped_value[-1])

    packaging_level: int | None = None
    prefix_value = stripped_value
    if gtin_format == GtinFormat.GTIN_14:
        packaging_level = int(stripped_value[0])
        prefix_value = stripped_value[1:]
    elif gtin_format == GtinFormat.GTIN_12:
        # Add a zero to convert U.P.C. Company Prefix to GS1 Company Prefix
        prefix_value = stripped_value.zfill(13)
    elif gtin_format == GtinFormat.GTIN_8:
        prefix_value = stripped_value.zfill(12)

    prefix = GS1Prefix.extract(prefix_value)
    company_prefix = GS1CompanyPrefix.extract(prefix_value)

    calculated_check_digit = gs1_standard_check_digit(payload)
    if check_digit != calculated_check_digit:
        msg = (
            f"Invalid GTIN check digit for {value!r}: "
            f"Expected {calculated_check_digit!r}, got {check_digit!r}."
        )
        raise ParseError(msg)

    gtin_type: type[Gtin | Rcn]
    if (
        gtin_format <= GtinFormat.GTIN_13
        and prefix is not None
        and "Restricted Circulation Number" in prefix.usage
    ):
        gtin_type = Rcn
    else:
        gtin_type = Gtin

    result = gtin_type(
        value=value,
        format=gtin_format,
        prefix=prefix,
        company_prefix=company_prefix,
        payload=payload,
        check_digit=check_digit,
        packaging_level=packaging_level,
    )

    if isinstance(result, Rcn):
        result = result._with_usage()  # noqa: SLF001
        result = result._parsed_with_regional_rules(  # noqa: SLF001
            config=config
        )

    return result

without_variable_measure

without_variable_measure() -> Gtin

Create a new RCN where the variable measure is zeroed out.

This provides us with a number which still includes the item reference, but does not vary with weight/price, and can thus be used to lookup the relevant trade item in a database or similar.

This has no effect on RCNs intended for use within a company, as the semantics of those numbers vary from company to company.

Returns:

  • Gtin

    A RCN instance with zeros in the variable measure places.

Raises:

  • EncodeError

    If the rules for variable measures in the region are unknown.

Source code in src/biip/rcn.py
def without_variable_measure(self) -> Gtin:
    """Create a new RCN where the variable measure is zeroed out.

    This provides us with a number which still includes the item
    reference, but does not vary with weight/price, and can thus be used
    to lookup the relevant trade item in a database or similar.

    This has no effect on RCNs intended for use within a company, as
    the semantics of those numbers vary from company to company.

    Returns:
        A RCN instance with zeros in the variable measure places.

    Raises:
        EncodeError: If the rules for variable measures in the region are unknown.
    """
    if self.usage == RcnUsage.COMPANY:
        # The value is an RCN, but it is intended for use within a company,
        # so we can only interpret it as an opaque GTIN.
        return self

    if self.region is None:
        msg = (
            f"Cannot zero out the variable measure part of {self.value!r} as the "
            f"RCN rules for the geographical region {self.region!r} are unknown."
        )
        raise EncodeError(msg)

    strategy = _Strategy.get_for_rcn(self)
    if strategy is None:
        # This prefix has no rules for removing variable parts.
        return self

    return strategy.without_variable_measure(self)

RcnRegion

Bases: Enum

Enum of geographical regions with custom RCN rules.

The value of the enum is the lowercase ISO 3166-1 Alpha-2 code.

Source code in src/biip/rcn.py
class RcnRegion(Enum):
    """Enum of geographical regions with custom RCN rules.

    The value of the enum is the lowercase ISO 3166-1 Alpha-2 code.
    """

    DENMARK = "dk"
    """Denmark"""

    ESTONIA = "ee"
    """Estonia"""

    FINLAND = "fi"
    """Finland"""

    GERMANY = "de"
    """Germany"""

    GREAT_BRITAIN = "gb"
    """Great Britain"""

    LATVIA = "lv"
    """Latvia"""

    LITHUANIA = "lt"
    """Lithuania"""

    NORWAY = "no"
    """Norway"""

    SWEDEN = "se"
    """Sweden"""

    def __repr__(self) -> str:
        """Canonical string representation of format."""
        return f"RcnRegion.{self.name}"

    def get_currency_code(self) -> str | None:
        """Get the ISO-4217 currency code for the region."""
        return {
            RcnRegion.DENMARK: "DKK",
            RcnRegion.GERMANY: "EUR",
            RcnRegion.GREAT_BRITAIN: "GBP",
            RcnRegion.NORWAY: "NOK",
            RcnRegion.SWEDEN: "SEK",
        }.get(self)

DENMARK class-attribute instance-attribute

DENMARK = 'dk'

Denmark

ESTONIA class-attribute instance-attribute

ESTONIA = 'ee'

Estonia

FINLAND class-attribute instance-attribute

FINLAND = 'fi'

Finland

GERMANY class-attribute instance-attribute

GERMANY = 'de'

Germany

GREAT_BRITAIN class-attribute instance-attribute

GREAT_BRITAIN = 'gb'

Great Britain

LATVIA class-attribute instance-attribute

LATVIA = 'lv'

Latvia

LITHUANIA class-attribute instance-attribute

LITHUANIA = 'lt'

Lithuania

NORWAY class-attribute instance-attribute

NORWAY = 'no'

Norway

SWEDEN class-attribute instance-attribute

SWEDEN = 'se'

Sweden

get_currency_code

get_currency_code() -> str | None

Get the ISO-4217 currency code for the region.

Source code in src/biip/rcn.py
def get_currency_code(self) -> str | None:
    """Get the ISO-4217 currency code for the region."""
    return {
        RcnRegion.DENMARK: "DKK",
        RcnRegion.GERMANY: "EUR",
        RcnRegion.GREAT_BRITAIN: "GBP",
        RcnRegion.NORWAY: "NOK",
        RcnRegion.SWEDEN: "SEK",
    }.get(self)

RcnUsage

Bases: Enum

Enum of RCN usage restrictions.

Source code in src/biip/rcn.py
class RcnUsage(Enum):
    """Enum of RCN usage restrictions."""

    GEOGRAPHICAL = "geo"
    """Usage of RCN restricted to geopgraphical area."""

    COMPANY = "company"
    """Usage of RCN restricted to internally in a company."""

    def __repr__(self) -> str:
        """Canonical string representation of format."""
        return f"RcnUsage.{self.name}"

COMPANY class-attribute instance-attribute

COMPANY = 'company'

Usage of RCN restricted to internally in a company.

GEOGRAPHICAL class-attribute instance-attribute

GEOGRAPHICAL = 'geo'

Usage of RCN restricted to geopgraphical area.