1use serde::Deserialize;
7
8use crate::models::{
9 AccountSubType, AccountType, ChartOfAccounts, CoAComplexity, GLAccount, IndustrySector,
10};
11
12pub type PcgRoot = Vec<PcgNode>;
14
15#[derive(Debug, Clone, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub struct PcgNode {
19 pub number: u32,
20 pub label: String,
21 #[serde(default)]
22 pub system: String,
23 #[serde(default)]
24 pub accounts: Vec<PcgNode>,
25}
26
27const PCG_2024_JSON: &str = include_str!("../resources/pcg_2024.json");
29
30pub fn load_pcg_2024() -> Result<PcgRoot, serde_json::Error> {
32 serde_json::from_str(PCG_2024_JSON)
33}
34
35fn flatten_pcg(
39 nodes: &[PcgNode],
40 class_from_prefix: u8,
41 out: &mut Vec<(u32, String, u8)>,
42 max_accounts: usize,
43) {
44 if out.len() >= max_accounts {
45 return;
46 }
47 for node in nodes {
48 let class = if node.number < 10 {
49 node.number as u8
50 } else {
51 class_from_prefix
52 };
53 let is_leaf = node.accounts.is_empty();
54 let is_postable = is_leaf
55 || node.system == "base"
56 || node.system == "developed"
57 || (node.system == "condensed" && node.accounts.is_empty());
58 if is_postable {
59 out.push((node.number, node.label.clone(), class));
60 }
61 if !node.accounts.is_empty() && out.len() < max_accounts {
62 flatten_pcg(&node.accounts, class, out, max_accounts);
63 }
64 }
65}
66
67fn normalize_pcg_account_number(number: u32) -> String {
76 let s = number.to_string();
77 if s.len() >= 6 {
78 return s;
79 }
80 let pow = (6 - s.len()) as u32;
81 let factor = 10u32.pow(pow);
82 format!("{:06}", number * factor)
83}
84
85fn pcg_subclass(number: u32) -> u32 {
87 let mut n = number;
88 while n >= 100 {
89 n /= 10;
90 }
91 n
92}
93
94fn pcg_account_group(number: u32) -> u32 {
96 let mut n = number;
97 while n >= 1000 {
98 n /= 10;
99 }
100 n
101}
102
103fn pcg_to_account_type(class: u8, number: u32) -> (AccountType, AccountSubType) {
105 use AccountSubType::{
106 AccountsPayable, AccountsReceivable, AccruedLiabilities, AccumulatedDepreciation, Cash,
107 CommonStock, FixedAssets, Inventory, LongTermDebt, OperatingExpenses, OtherAssets,
108 OtherLiabilities, ProductRevenue, RetainedEarnings, SuspenseClearing,
109 };
110 use AccountType::{Asset, Equity, Expense, Liability, Revenue};
111 let sub = pcg_subclass(number);
112 match class {
113 1 => {
114 if (10..=14).contains(&sub) {
115 let group = pcg_account_group(number);
116 if (101..=109).contains(&group) {
117 (Equity, CommonStock)
118 } else {
119 (Equity, RetainedEarnings)
120 }
121 } else if sub == 15 {
122 (Liability, AccruedLiabilities)
123 } else if (16..=17).contains(&sub) {
124 (Liability, LongTermDebt)
125 } else {
126 (Liability, OtherLiabilities)
127 }
128 }
129 2 => {
130 if (28..=29).contains(&sub) {
131 (Asset, AccumulatedDepreciation)
132 } else {
133 (Asset, FixedAssets)
134 }
135 }
136 3 => (Asset, Inventory),
137 4 => {
138 if sub == 40 {
139 (Liability, AccountsPayable)
140 } else if sub == 41 {
141 (Asset, AccountsReceivable)
142 } else if sub == 42 {
143 (Liability, AccruedLiabilities)
144 } else {
145 (Liability, OtherLiabilities)
146 }
147 }
148 5 => (Asset, Cash),
149 6 => (Expense, OperatingExpenses),
150 7 => (Revenue, ProductRevenue),
151 8 => (Asset, SuspenseClearing),
152 _ => (Asset, OtherAssets),
153 }
154}
155
156pub fn build_chart_of_accounts_from_pcg_2024(
159 complexity: CoAComplexity,
160 industry: IndustrySector,
161) -> Result<ChartOfAccounts, serde_json::Error> {
162 let root = load_pcg_2024()?;
163 let max_accounts = complexity.target_count();
164 let mut flat = Vec::with_capacity(max_accounts.min(5000));
165 for class_node in &root {
166 let class = class_node.number as u8;
167 flatten_pcg(&class_node.accounts, class, &mut flat, max_accounts);
168 }
169
170 let coa_id = format!("COA_PCG_2024_{:?}_{}", industry, max_accounts);
171 let name = format!("Plan Comptable Général 2024 – {:?}", industry);
172 let mut coa = ChartOfAccounts::new(coa_id, name, "FR".to_string(), industry, complexity);
173 coa.account_format = "######".to_string();
174
175 for (number, label, class) in flat {
176 let code = normalize_pcg_account_number(number);
177 let (acc_type, sub_type) = pcg_to_account_type(class, number);
178 let mut account = GLAccount::new(code, label, acc_type, sub_type);
179 account.requires_cost_center = acc_type == AccountType::Expense;
180 if class == 8 {
181 account.is_suspense_account = true;
182 }
183 coa.add_account(account);
184 }
185
186 Ok(coa)
187}
188
189#[cfg(test)]
190#[allow(clippy::unwrap_used)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_load_pcg_2024() {
196 let root = load_pcg_2024().unwrap();
197 assert_eq!(root.len(), 8); assert_eq!(root[0].number, 1);
199 assert_eq!(root[0].label, "Comptes de capitaux");
200 }
201
202 #[test]
203 fn test_pcg_subclass() {
204 assert_eq!(super::pcg_subclass(10), 10);
205 assert_eq!(super::pcg_subclass(101), 10);
206 assert_eq!(super::pcg_subclass(1011), 10);
207 assert_eq!(super::pcg_subclass(164), 16);
208 assert_eq!(super::pcg_subclass(4111), 41);
209 assert_eq!(super::pcg_subclass(28), 28);
210 assert_eq!(super::pcg_subclass(281), 28);
211 }
212
213 #[test]
214 fn test_pcg_account_group() {
215 assert_eq!(super::pcg_account_group(101), 101);
216 assert_eq!(super::pcg_account_group(1011), 101);
217 assert_eq!(super::pcg_account_group(10131), 101);
218 assert_eq!(super::pcg_account_group(164), 164);
219 assert_eq!(super::pcg_account_group(4111), 411);
220 }
221
222 #[test]
223 fn test_pcg_to_account_type_multidigit() {
224 use crate::models::{AccountSubType, AccountType};
225 let (ty, sub) = super::pcg_to_account_type(1, 1011);
227 assert_eq!(ty, AccountType::Equity);
228 assert_eq!(sub, AccountSubType::CommonStock);
229
230 let (ty, sub) = super::pcg_to_account_type(1, 129);
232 assert_eq!(ty, AccountType::Equity);
233 assert_eq!(sub, AccountSubType::RetainedEarnings);
234
235 let (ty, sub) = super::pcg_to_account_type(1, 1641);
237 assert_eq!(ty, AccountType::Liability);
238 assert_eq!(sub, AccountSubType::LongTermDebt);
239
240 let (ty, sub) = super::pcg_to_account_type(1, 151);
242 assert_eq!(ty, AccountType::Liability);
243 assert_eq!(sub, AccountSubType::AccruedLiabilities);
244
245 let (ty, sub) = super::pcg_to_account_type(2, 2815);
247 assert_eq!(ty, AccountType::Asset);
248 assert_eq!(sub, AccountSubType::AccumulatedDepreciation);
249
250 let (ty, sub) = super::pcg_to_account_type(4, 4111);
252 assert_eq!(ty, AccountType::Asset);
253 assert_eq!(sub, AccountSubType::AccountsReceivable);
254
255 let (ty, sub) = super::pcg_to_account_type(4, 4011);
257 assert_eq!(ty, AccountType::Liability);
258 assert_eq!(sub, AccountSubType::AccountsPayable);
259
260 let (ty, sub) = super::pcg_to_account_type(4, 421);
262 assert_eq!(ty, AccountType::Liability);
263 assert_eq!(sub, AccountSubType::AccruedLiabilities);
264 }
265
266 #[test]
267 fn test_build_coa_from_pcg() {
268 let coa = build_chart_of_accounts_from_pcg_2024(
269 CoAComplexity::Small,
270 IndustrySector::Manufacturing,
271 )
272 .unwrap();
273 assert_eq!(coa.country, "FR");
274 assert!(coa.account_count() >= 50);
275 assert!(coa.account_count() <= 150);
276 }
277}