@@ -47,13 +47,35 @@ class StandardConcept(str, Enum):
4747 RETAINED_EARNINGS = "Retained Earnings"
4848 TOTAL_EQUITY = "Total Stockholders' Equity"
4949
50- # Income Statement
50+ # Income Statement - Revenue Hierarchy
5151 REVENUE = "Revenue"
52+ PRODUCT_REVENUE = "Product Revenue"
53+ SERVICE_REVENUE = "Service Revenue"
54+ SUBSCRIPTION_REVENUE = "Subscription Revenue"
55+ LEASING_REVENUE = "Leasing Revenue"
56+
57+ # Industry-Specific Revenue Concepts
58+ AUTOMOTIVE_REVENUE = "Automotive Revenue"
59+ AUTOMOTIVE_LEASING_REVENUE = "Automotive Leasing Revenue"
60+ ENERGY_REVENUE = "Energy Revenue"
61+ SOFTWARE_REVENUE = "Software Revenue"
62+ HARDWARE_REVENUE = "Hardware Revenue"
63+ PLATFORM_REVENUE = "Platform Revenue"
64+
65+ # Income Statement - Expenses
5266 COST_OF_REVENUE = "Cost of Revenue"
5367 GROSS_PROFIT = "Gross Profit"
5468 OPERATING_EXPENSES = "Operating Expenses"
5569 RESEARCH_AND_DEVELOPMENT = "Research and Development Expense"
70+
71+ # Enhanced Expense Hierarchy
5672 SELLING_GENERAL_ADMIN = "Selling, General and Administrative Expense"
73+ SELLING_EXPENSE = "Selling Expense"
74+ GENERAL_ADMIN_EXPENSE = "General and Administrative Expense"
75+ MARKETING_EXPENSE = "Marketing Expense"
76+ SALES_EXPENSE = "Sales Expense"
77+
78+ # Other Income Statement
5779 OPERATING_INCOME = "Operating Income"
5880 INTEREST_EXPENSE = "Interest Expense"
5981 INCOME_BEFORE_TAX = "Income Before Tax"
@@ -100,6 +122,8 @@ class MappingStore:
100122 Attributes:
101123 source (str): Path to the JSON file storing the mappings
102124 mappings (Dict[str, Set[str]]): Dictionary mapping standard concepts to sets of company concepts
125+ company_mappings (Dict[str, Dict]): Company-specific mappings loaded from company_mappings/
126+ merged_mappings (Dict[str, List[Tuple]]): Merged mappings with priority scoring
103127 """
104128
105129 def __init__ (self , source : Optional [str ] = None , validate_with_enum : bool = False , read_only : bool = False ):
@@ -113,6 +137,7 @@ def __init__(self, source: Optional[str] = None, validate_with_enum: bool = Fals
113137 """
114138 self .read_only = read_only
115139
140+
116141 if source is None :
117142 # Try a few different ways to locate the file, handling both development
118143 # and installed package scenarios
@@ -130,15 +155,15 @@ def __init__(self, source: Optional[str] = None, validate_with_enum: bool = Fals
130155 import importlib .resources as pkg_resources
131156 try :
132157 # For Python 3.9+
133- with pkg_resources .files ('edgar.xbrl2 .standardization' ).joinpath ('concept_mappings.json' ).open ('r' ) as f :
158+ with pkg_resources .files ('edgar.xbrl .standardization' ).joinpath ('concept_mappings.json' ).open ('r' ) as f :
134159 # Just read the file to see if it exists, we'll load it properly later
135160 f .read (1 )
136161 self .source = potential_path # Use the same path as before
137162 except (ImportError , FileNotFoundError , AttributeError ):
138163 # Fallback for older Python versions
139164 try :
140165 import pkg_resources as legacy_resources
141- if legacy_resources .resource_exists ('edgar.xbrl2 .standardization' , 'concept_mappings.json' ):
166+ if legacy_resources .resource_exists ('edgar.xbrl .standardization' , 'concept_mappings.json' ):
142167 self .source = potential_path # Use the same path as before
143168 except (ImportError , FileNotFoundError ):
144169 pass
@@ -154,6 +179,11 @@ def __init__(self, source: Optional[str] = None, validate_with_enum: bool = Fals
154179
155180 self .mappings = self ._load_mappings ()
156181
182+ # Load company-specific mappings (always enabled)
183+ self .company_mappings = self ._load_all_company_mappings ()
184+ self .merged_mappings = self ._create_merged_mappings ()
185+ self .hierarchy_rules = self ._load_hierarchy_rules ()
186+
157187 # Validate the loaded mappings against StandardConcept enum
158188 if validate_with_enum :
159189 self .validate_against_enum ()
@@ -207,6 +237,79 @@ def to_dataframe(self) -> pd.DataFrame:
207237
208238 return pd .DataFrame (rows )
209239
240+
241+ def _load_all_company_mappings (self ) -> Dict [str , Dict ]:
242+ """Load all company-specific mapping files from company_mappings/ directory."""
243+ mappings = {}
244+ company_dir = os .path .join (os .path .dirname (self .source or __file__ ), "company_mappings" )
245+
246+ if os .path .exists (company_dir ):
247+ for file in os .listdir (company_dir ):
248+ if file .endswith ("_mappings.json" ):
249+ entity_id = file .replace ("_mappings.json" , "" )
250+ try :
251+ with open (os .path .join (company_dir , file ), 'r' ) as f :
252+ company_data = json .load (f )
253+ mappings [entity_id ] = company_data
254+ except (FileNotFoundError , json .JSONDecodeError ) as e :
255+ import logging
256+ logger = logging .getLogger (__name__ )
257+ logger .warning (f"Failed to load { file } : { e } " )
258+
259+ return mappings
260+
261+ def _create_merged_mappings (self ) -> Dict [str , List [Tuple [str , str , int ]]]:
262+ """Create merged mappings with priority scoring.
263+
264+ Priority levels:
265+ 1. Core mappings (lowest)
266+ 2. Company mappings (higher)
267+ 3. Company-specific matches (highest when company detected)
268+
269+ Returns:
270+ Dict mapping standard concepts to list of (company_concept, source, priority) tuples
271+ """
272+ merged = {}
273+
274+ # Add core mappings (priority 1 - lowest)
275+ for std_concept , company_concepts in self .mappings .items ():
276+ merged [std_concept ] = []
277+ for concept in company_concepts :
278+ merged [std_concept ].append ((concept , "core" , 1 ))
279+
280+ # Add company mappings (priority 2 - higher)
281+ for entity_id , company_data in self .company_mappings .items ():
282+ concept_mappings = company_data .get ("concept_mappings" , {})
283+ priority_level = 2
284+
285+ for std_concept , company_concepts in concept_mappings .items ():
286+ if std_concept not in merged :
287+ merged [std_concept ] = []
288+ for concept in company_concepts :
289+ merged [std_concept ].append ((concept , entity_id , priority_level ))
290+
291+ return merged
292+
293+ def _load_hierarchy_rules (self ) -> Dict [str , Dict ]:
294+ """Load hierarchy rules from company mappings."""
295+ all_rules = {}
296+
297+ # Add company hierarchy rules
298+ for entity_id , company_data in self .company_mappings .items ():
299+ hierarchy_rules = company_data .get ("hierarchy_rules" , {})
300+ all_rules .update (hierarchy_rules )
301+
302+ return all_rules
303+
304+ def _detect_entity_from_concept (self , concept : str ) -> Optional [str ]:
305+ """Detect entity identifier from concept name prefix."""
306+ if ':' in concept :
307+ prefix = concept .split (':' )[0 ].lower ()
308+ # Check if this prefix corresponds to a known company
309+ if prefix in self .company_mappings :
310+ return prefix
311+ return None
312+
210313 def _load_mappings (self ) -> Dict [str , Set [str ]]:
211314 """
212315 Load mappings from the JSON file.
@@ -228,12 +331,12 @@ def _load_mappings(self) -> Dict[str, Set[str]]:
228331 import importlib .resources as pkg_resources
229332 try :
230333 # For Python 3.9+
231- with pkg_resources .files ('edgar.xbrl2 .standardization' ).joinpath ('concept_mappings.json' ).open ('r' ) as f :
334+ with pkg_resources .files ('edgar.xbrl .standardization' ).joinpath ('concept_mappings.json' ).open ('r' ) as f :
232335 data = json .load (f )
233336 except (ImportError , FileNotFoundError , AttributeError ):
234337 # Fallback to legacy pkg_resources
235338 import pkg_resources as legacy_resources
236- resource_string = legacy_resources .resource_string ('edgar.xbrl2 .standardization' , 'concept_mappings.json' )
339+ resource_string = legacy_resources .resource_string ('edgar.xbrl .standardization' , 'concept_mappings.json' )
237340 data = json .loads (resource_string )
238341 except ImportError :
239342 pass
@@ -292,16 +395,44 @@ def add(self, company_concept: str, standard_concept: str) -> None:
292395 self .mappings [standard_concept ].add (company_concept )
293396 self ._save_mappings ()
294397
295- def get_standard_concept (self , company_concept : str ) -> Optional [str ]:
398+ def get_standard_concept (self , company_concept : str , context : Dict = None ) -> Optional [str ]:
296399 """
297- Get the standard concept for a given company concept.
400+ Get the standard concept for a given company concept with priority-based resolution .
298401
299402 Args:
300403 company_concept: The company-specific concept
404+ context: Optional context information (not used in current implementation)
301405
302406 Returns:
303407 The standard concept or None if not found
304408 """
409+ # Use merged mappings with priority-based resolution
410+ if self .merged_mappings :
411+ # Detect company from concept prefix (e.g., 'tsla:Revenue' -> 'tsla')
412+ detected_entity = self ._detect_entity_from_concept (company_concept )
413+
414+ # Search through merged mappings with priority
415+ candidates = []
416+
417+ for std_concept , mapping_list in self .merged_mappings .items ():
418+ for concept , source , priority in mapping_list :
419+ if concept == company_concept :
420+ # Boost priority if it matches detected entity
421+ effective_priority = priority
422+ if detected_entity and source == detected_entity :
423+ effective_priority = 4 # Highest priority for exact company match
424+
425+ candidates .append ((std_concept , effective_priority , source ))
426+
427+ # Return highest priority match
428+ if candidates :
429+ best_match = max (candidates , key = lambda x : x [1 ])
430+ import logging
431+ logger = logging .getLogger (__name__ )
432+ logger .debug (f"Mapping applied: { company_concept } -> { best_match [0 ]} (source: { best_match [2 ]} , priority: { best_match [1 ]} )" )
433+ return best_match [0 ]
434+
435+ # Fallback to core mappings
305436 for standard_concept , company_concepts in self .mappings .items ():
306437 if company_concept in company_concepts :
307438 return standard_concept
0 commit comments