diff --git a/src/vininfo/brands.py b/src/vininfo/brands.py index 00796a6..b84dcdb 100644 --- a/src/vininfo/brands.py +++ b/src/vininfo/brands.py @@ -29,4 +29,15 @@ class Dafra(Brand): class Bajaj(Brand): - extractor = BajajDetails \ No newline at end of file + extractor = BajajDetails + + +class FordAustralia(Brand): + + extractor = FordAustraliaDetails + + # Year letter at VIN position 11 (vis index 1), not SAE position 10. + year_position = 1 + + # Position 9 is a model code, not an SAE check digit. + uses_sae_checkdigit = False \ No newline at end of file diff --git a/src/vininfo/common.py b/src/vininfo/common.py index d003083..d0b1940 100644 --- a/src/vininfo/common.py +++ b/src/vininfo/common.py @@ -76,6 +76,12 @@ def __str__(self): class Brand(Assembler): extractor: type['VinDetails'] = None + # VIS index of the model-year letter. SAE J853 default is 0 (= VIN pos 10). + year_position: int = 0 + + # SAE J853 check digit at position 9 applies to this brand. + uses_sae_checkdigit: bool = True + @property def brands(self) -> set['Brand']: return {self} diff --git a/src/vininfo/details/__init__.py b/src/vininfo/details/__init__.py index 9e1d6b0..469a731 100644 --- a/src/vininfo/details/__init__.py +++ b/src/vininfo/details/__init__.py @@ -1,11 +1,18 @@ from .avtovaz import AvtoVazDetails from .bajaj import BajajDetails from .dafra import DafraDetails +from .ford import FordAustraliaDetails from .nissan import NissanDetails from .opel import OpelDetails from .renault import RenaultDetails __all__ = [ - 'AvtoVazDetails', 'BajajDetails', 'DafraDetails', 'NissanDetails', 'OpelDetails', 'RenaultDetails' + 'AvtoVazDetails', + 'BajajDetails', + 'DafraDetails', + 'FordAustraliaDetails', + 'NissanDetails', + 'OpelDetails', + 'RenaultDetails' ] diff --git a/src/vininfo/details/ford.py b/src/vininfo/details/ford.py new file mode 100644 index 0000000..994b313 --- /dev/null +++ b/src/vininfo/details/ford.py @@ -0,0 +1,36 @@ +from ._base import Detail, VinDetails + + +class FordAustraliaDetails(VinDetails): + """Ford Australia VIN details extractor.""" + + platform = Detail(('vds', 3), { + 'A': 'North America', + 'C': 'Europe / Britain', + 'J': 'Australia', + 'U': 'Japan (Mazda)', + }) + + plant = Detail(('vds', 4), { + 'G': 'Broadmeadows (main line)', + 'H': 'Brisbane', + 'K': 'Sydney', + 'L': 'Broadmeadows (secondary line)', + }) + + # Position 9 + 10 hold a 2-char body code, but the Detail descriptor + # reads from a single section (vds ends at 9, vis starts at 10), so + # split: body_class = pos 9, body_subclass = pos 10. + body_class = Detail(('vds', 5), { + 'A': 'Territory', + 'C': 'Commercial / Ute', + 'S': 'Sedan / Wagon', + }) + body_subclass = Detail(('vis', 0)) + + # Brand binding in Vin.__init__ requires details.model.name to be truthy. + model = body_class + + month = Detail(('vis', 2)) + + serial = Detail(('vis', slice(3, None))) diff --git a/src/vininfo/dicts/wmi.py b/src/vininfo/dicts/wmi.py index 6c2b8ec..431665f 100644 --- a/src/vininfo/dicts/wmi.py +++ b/src/vininfo/dicts/wmi.py @@ -1,5 +1,5 @@ from ..assemblers import Dafra -from ..brands import Bajaj, Lada, Nissan, Opel, Renault +from ..brands import Bajaj, FordAustralia, Lada, Nissan, Opel, Renault # NOTE: # if you want to extend this mapping with new WMIs, please use @@ -249,7 +249,7 @@ '6F': 'Ford', '6F4': Nissan('Nissan Motor Company'), '6F5': 'Kenworth', - '6FP': 'Ford Motor Company', + '6FP': FordAustralia('Ford Australia'), '6G': 'General Motors', '6G1': 'Chevrolet', '6G2': 'Pontiac', diff --git a/src/vininfo/toolbox.py b/src/vininfo/toolbox.py index 57b51dc..ee3c3aa 100644 --- a/src/vininfo/toolbox.py +++ b/src/vininfo/toolbox.py @@ -69,6 +69,9 @@ def verify_checksum(self, *, check_year: bool = True) -> bool: Note that not all manufacturer abey the rule. Default: True. """ + if not getattr(self.assembler, 'uses_sae_checkdigit', True): + return False + if check_year and self.vis[0] in {'U', 'Z', '0'}: return False @@ -168,7 +171,7 @@ def country(self) -> str | None: @property def years_code(self) -> str: - return self.vis[0] + return self.vis[getattr(self.assembler, 'year_position', 0)] @property def years(self) -> list[int]: diff --git a/tests/test_ford_australia.py b/tests/test_ford_australia.py new file mode 100644 index 0000000..9642d41 --- /dev/null +++ b/tests/test_ford_australia.py @@ -0,0 +1,55 @@ +from vininfo import Vin + + +def test_ford_au_year_at_position_11_not_10(): + """Synthetic 2004 Territory.""" + vin = Vin('6FPAAAJGAT4Z00001') + + assert vin.wmi == '6FP' + assert vin.manufacturer == 'Ford Australia' + assert vin.country == 'Australia' + + # Year is decoded from VIS index 1 (= VIN position 11), not the + # SAE-default VIS index 0 (position 10). + assert vin.years_code == '4' + assert vin.years == [2004] + + # Position 9 ('A') is a model/body class code, not an SAE check digit, + # so the global checksum check never matches for this brand. + assert vin.verify_checksum() is False + + details = vin.details + assert details.platform.code == 'J' + assert details.platform.name == 'Australia' + assert details.plant.code == 'G' + assert details.plant.name == 'Broadmeadows (main line)' + assert details.body_class.code == 'A' + assert details.body_class.name == 'Territory' + assert details.body_subclass.code == 'T' + assert details.serial.code == '00001' + + +def test_ford_au_falcon_ute_year_letter_b_means_2011(): + """Synthetic 2011 Falcon Ute""" + vin = Vin('6FPAAAJGCMBZ00002') + assert vin.manufacturer == 'Ford Australia' + assert vin.years_code == 'B' + assert 2011 in vin.years + assert vin.details.body_class.name == 'Commercial / Ute' + assert vin.details.plant.name == 'Broadmeadows (main line)' + + +def test_ford_au_falcon_sedan_year_digit_2_means_2002(): + """Synthetic 2002 Falcon Sedan""" + vin = Vin('6FPAAAJGSW2Z00003') + assert vin.years_code == '2' + assert vin.years == [2002] + assert vin.details.body_class.name == 'Sedan / Wagon' + + +def test_ford_au_secondary_line_pre_2000(): + """Synthetic pre-2000 Falcon built on the Broadmeadows secondary line""" + vin = Vin('6FPAAAJL0MSZ00004') + assert vin.years_code == 'S' + assert 1995 in vin.years + assert vin.details.plant.name == 'Broadmeadows (secondary line)'