Skip to main content

datasynth_core/models/
company.rs

1//! Company code and organizational structures.
2//!
3//! Defines the company code entity which represents a legal entity
4//! or organizational unit within an enterprise group.
5
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8
9use crate::models::intercompany::ConsolidationMethod;
10
11/// Fiscal year variant defining the fiscal calendar.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct FiscalYearVariant {
14    /// Variant code (e.g., "K4" for calendar year)
15    pub code: String,
16    /// Description
17    pub description: String,
18    /// Number of posting periods (typically 12)
19    pub periods: u8,
20    /// Number of special periods (typically 4)
21    pub special_periods: u8,
22    /// First month of fiscal year (1-12)
23    pub first_month: u8,
24}
25
26impl FiscalYearVariant {
27    /// Calendar year fiscal variant (Jan-Dec).
28    pub fn calendar_year() -> Self {
29        Self {
30            code: "K4".to_string(),
31            description: "Calendar Year".to_string(),
32            periods: 12,
33            special_periods: 4,
34            first_month: 1,
35        }
36    }
37
38    /// US federal fiscal year (Oct-Sep).
39    pub fn us_federal() -> Self {
40        Self {
41            code: "V3".to_string(),
42            description: "US Federal Fiscal Year".to_string(),
43            periods: 12,
44            special_periods: 4,
45            first_month: 10,
46        }
47    }
48
49    /// April fiscal year (Apr-Mar).
50    pub fn april_year() -> Self {
51        Self {
52            code: "K1".to_string(),
53            description: "April Fiscal Year".to_string(),
54            periods: 12,
55            special_periods: 4,
56            first_month: 4,
57        }
58    }
59}
60
61impl Default for FiscalYearVariant {
62    fn default() -> Self {
63        Self::calendar_year()
64    }
65}
66
67/// Company code representing a legal entity or organizational unit.
68///
69/// In SAP terminology, a company code is the smallest organizational unit
70/// for which a complete self-contained set of accounts can be drawn up
71/// for external reporting.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CompanyCode {
74    /// Company code identifier (typically 4 characters)
75    pub code: String,
76
77    /// Company name
78    pub name: String,
79
80    /// Legal name (for official documents)
81    pub legal_name: String,
82
83    /// Local/functional currency (ISO 4217)
84    pub currency: String,
85
86    /// Country code (ISO 3166-1 alpha-2)
87    pub country: String,
88
89    /// City
90    pub city: Option<String>,
91
92    /// Fiscal year variant
93    pub fiscal_year_variant: FiscalYearVariant,
94
95    /// Chart of accounts ID used by this company
96    pub coa_id: String,
97
98    /// Parent company code (for group structure)
99    pub parent_company: Option<String>,
100
101    /// Is this the group parent/consolidation entity
102    pub is_group_parent: bool,
103
104    /// Controlling area for cost accounting
105    pub controlling_area: Option<String>,
106
107    /// Credit control area
108    pub credit_control_area: Option<String>,
109
110    /// Time zone
111    pub time_zone: String,
112
113    /// VAT registration number
114    pub vat_number: Option<String>,
115
116    /// Tax jurisdiction
117    pub tax_jurisdiction: Option<String>,
118}
119
120impl CompanyCode {
121    /// Create a new company code with required fields.
122    pub fn new(code: String, name: String, currency: String, country: String) -> Self {
123        Self {
124            code: code.clone(),
125            name: name.clone(),
126            legal_name: name,
127            currency,
128            country,
129            city: None,
130            fiscal_year_variant: FiscalYearVariant::default(),
131            coa_id: "OPER".to_string(),
132            parent_company: None,
133            is_group_parent: false,
134            controlling_area: Some(code.clone()),
135            credit_control_area: Some(code),
136            time_zone: "UTC".to_string(),
137            vat_number: None,
138            tax_jurisdiction: None,
139        }
140    }
141
142    /// Create a US company code.
143    pub fn us(code: &str, name: &str) -> Self {
144        Self::new(
145            code.to_string(),
146            name.to_string(),
147            "USD".to_string(),
148            "US".to_string(),
149        )
150    }
151
152    /// Create a German company code.
153    pub fn de(code: &str, name: &str) -> Self {
154        Self::new(
155            code.to_string(),
156            name.to_string(),
157            "EUR".to_string(),
158            "DE".to_string(),
159        )
160    }
161
162    /// Create a UK company code.
163    pub fn gb(code: &str, name: &str) -> Self {
164        Self::new(
165            code.to_string(),
166            name.to_string(),
167            "GBP".to_string(),
168            "GB".to_string(),
169        )
170    }
171
172    /// Create a Swiss company code.
173    pub fn ch(code: &str, name: &str) -> Self {
174        Self::new(
175            code.to_string(),
176            name.to_string(),
177            "CHF".to_string(),
178            "CH".to_string(),
179        )
180    }
181}
182
183/// Enterprise group structure containing multiple company codes.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct EnterpriseGroup {
186    /// Group identifier
187    pub group_id: String,
188
189    /// Group name
190    pub name: String,
191
192    /// Group consolidation currency
193    pub group_currency: String,
194
195    /// Company codes in the group
196    pub companies: Vec<CompanyCode>,
197
198    /// Intercompany relationships (from_code, to_code)
199    pub intercompany_links: Vec<(String, String)>,
200}
201
202impl EnterpriseGroup {
203    /// Create a new enterprise group.
204    pub fn new(group_id: String, name: String, group_currency: String) -> Self {
205        Self {
206            group_id,
207            name,
208            group_currency,
209            companies: Vec::new(),
210            intercompany_links: Vec::new(),
211        }
212    }
213
214    /// Add a company to the group.
215    pub fn add_company(&mut self, company: CompanyCode) {
216        self.companies.push(company);
217    }
218
219    /// Add an intercompany link.
220    pub fn add_intercompany_link(&mut self, from_code: String, to_code: String) {
221        self.intercompany_links.push((from_code, to_code));
222    }
223
224    /// Get company by code.
225    pub fn get_company(&self, code: &str) -> Option<&CompanyCode> {
226        self.companies.iter().find(|c| c.code == code)
227    }
228
229    /// Get all company codes.
230    pub fn company_codes(&self) -> Vec<&str> {
231        self.companies.iter().map(|c| c.code.as_str()).collect()
232    }
233
234    /// Get intercompany partners for a company.
235    pub fn get_intercompany_partners(&self, code: &str) -> Vec<&str> {
236        self.intercompany_links
237            .iter()
238            .filter_map(|(from, to)| {
239                if from == code {
240                    Some(to.as_str())
241                } else if to == code {
242                    Some(from.as_str())
243                } else {
244                    None
245                }
246            })
247            .collect()
248    }
249}
250
251/// Company entity for graph building and consolidation.
252///
253/// This is a simplified view of a company entity with ownership and
254/// consolidation attributes for building entity relationship graphs.
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Company {
257    /// Company code identifier
258    pub company_code: String,
259
260    /// Company name
261    pub company_name: String,
262
263    /// Country code (ISO 3166-1 alpha-2)
264    pub country: String,
265
266    /// Local/functional currency (ISO 4217)
267    pub local_currency: String,
268
269    /// Functional currency for translation
270    pub functional_currency: String,
271
272    /// Is this the parent/holding company
273    pub is_parent: bool,
274
275    /// Parent company code (if subsidiary)
276    pub parent_company: Option<String>,
277
278    /// Ownership percentage (0-100)
279    pub ownership_percentage: Option<Decimal>,
280
281    /// Consolidation method
282    pub consolidation_method: ConsolidationMethod,
283}
284
285impl Company {
286    /// Create a new company entity.
287    pub fn new(
288        company_code: impl Into<String>,
289        company_name: impl Into<String>,
290        country: impl Into<String>,
291        local_currency: impl Into<String>,
292    ) -> Self {
293        let currency = local_currency.into();
294        Self {
295            company_code: company_code.into(),
296            company_name: company_name.into(),
297            country: country.into(),
298            local_currency: currency.clone(),
299            functional_currency: currency,
300            is_parent: false,
301            parent_company: None,
302            ownership_percentage: None,
303            consolidation_method: ConsolidationMethod::Full,
304        }
305    }
306
307    /// Create a parent company.
308    pub fn parent(
309        company_code: impl Into<String>,
310        company_name: impl Into<String>,
311        country: impl Into<String>,
312        currency: impl Into<String>,
313    ) -> Self {
314        let mut company = Self::new(company_code, company_name, country, currency);
315        company.is_parent = true;
316        company
317    }
318
319    /// Create a subsidiary company.
320    pub fn subsidiary(
321        company_code: impl Into<String>,
322        company_name: impl Into<String>,
323        country: impl Into<String>,
324        currency: impl Into<String>,
325        parent_code: impl Into<String>,
326        ownership_pct: Decimal,
327    ) -> Self {
328        let mut company = Self::new(company_code, company_name, country, currency);
329        company.parent_company = Some(parent_code.into());
330        company.ownership_percentage = Some(ownership_pct);
331        company
332    }
333}
334
335impl From<&CompanyCode> for Company {
336    fn from(cc: &CompanyCode) -> Self {
337        Self {
338            company_code: cc.code.clone(),
339            company_name: cc.name.clone(),
340            country: cc.country.clone(),
341            local_currency: cc.currency.clone(),
342            functional_currency: cc.currency.clone(),
343            is_parent: cc.is_group_parent,
344            parent_company: cc.parent_company.clone(),
345            ownership_percentage: None,
346            consolidation_method: ConsolidationMethod::Full,
347        }
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_company_creation() {
357        let company = CompanyCode::us("1000", "US Operations");
358        assert_eq!(company.code, "1000");
359        assert_eq!(company.currency, "USD");
360        assert_eq!(company.country, "US");
361    }
362
363    #[test]
364    fn test_enterprise_group() {
365        let mut group = EnterpriseGroup::new(
366            "CORP".to_string(),
367            "Global Corp".to_string(),
368            "USD".to_string(),
369        );
370
371        group.add_company(CompanyCode::us("1000", "US HQ"));
372        group.add_company(CompanyCode::de("2000", "EU Operations"));
373        group.add_intercompany_link("1000".to_string(), "2000".to_string());
374
375        assert_eq!(group.companies.len(), 2);
376        assert_eq!(group.get_intercompany_partners("1000"), vec!["2000"]);
377    }
378}