lambda-erp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. api/__init__.py +0 -0
  2. api/attachments.py +229 -0
  3. api/auth.py +511 -0
  4. api/bootstrap.py +498 -0
  5. api/chat.py +2764 -0
  6. api/demo_limits.py +400 -0
  7. api/deps.py +7 -0
  8. api/errors.py +56 -0
  9. api/main.py +182 -0
  10. api/pdf.py +151 -0
  11. api/providers.py +116 -0
  12. api/routers/__init__.py +0 -0
  13. api/routers/accounting.py +63 -0
  14. api/routers/admin.py +122 -0
  15. api/routers/analytics.py +1009 -0
  16. api/routers/bank_reconciliation.py +31 -0
  17. api/routers/documents.py +100 -0
  18. api/routers/masters.py +396 -0
  19. api/routers/reports.py +735 -0
  20. api/routers/setup.py +387 -0
  21. api/services.py +372 -0
  22. api/templates/document.html +197 -0
  23. lambda_erp/__init__.py +3 -0
  24. lambda_erp/accounting/__init__.py +0 -0
  25. lambda_erp/accounting/bank_transaction.py +76 -0
  26. lambda_erp/accounting/budget.py +117 -0
  27. lambda_erp/accounting/chart_of_accounts.py +183 -0
  28. lambda_erp/accounting/general_ledger.py +362 -0
  29. lambda_erp/accounting/journal_entry.py +235 -0
  30. lambda_erp/accounting/payment_entry.py +515 -0
  31. lambda_erp/accounting/pos_invoice.py +342 -0
  32. lambda_erp/accounting/purchase_invoice.py +504 -0
  33. lambda_erp/accounting/revaluation.py +172 -0
  34. lambda_erp/accounting/sales_invoice.py +523 -0
  35. lambda_erp/accounting/subscription.py +132 -0
  36. lambda_erp/buying/__init__.py +0 -0
  37. lambda_erp/buying/purchase_order.py +165 -0
  38. lambda_erp/controllers/__init__.py +0 -0
  39. lambda_erp/controllers/currency.py +52 -0
  40. lambda_erp/controllers/defaults.py +51 -0
  41. lambda_erp/controllers/pricing_rule.py +103 -0
  42. lambda_erp/controllers/taxes_and_totals.py +369 -0
  43. lambda_erp/database.py +1543 -0
  44. lambda_erp/exceptions.py +37 -0
  45. lambda_erp/hooks.py +37 -0
  46. lambda_erp/model.py +462 -0
  47. lambda_erp/selling/__init__.py +0 -0
  48. lambda_erp/selling/quotation.py +263 -0
  49. lambda_erp/selling/sales_order.py +214 -0
  50. lambda_erp/simulation.py +704 -0
  51. lambda_erp/stock/__init__.py +0 -0
  52. lambda_erp/stock/delivery_note.py +254 -0
  53. lambda_erp/stock/purchase_receipt.py +356 -0
  54. lambda_erp/stock/stock_entry.py +330 -0
  55. lambda_erp/stock/stock_ledger.py +337 -0
  56. lambda_erp/utils.py +167 -0
  57. lambda_erp-0.1.0.dist-info/METADATA +454 -0
  58. lambda_erp-0.1.0.dist-info/RECORD +60 -0
  59. lambda_erp-0.1.0.dist-info/WHEEL +4 -0
  60. lambda_erp-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,37 @@
1
+ """Custom exceptions for Lambda ERP."""
2
+
3
+
4
+ class ValidationError(Exception):
5
+ pass
6
+
7
+
8
+ class MandatoryError(ValidationError):
9
+ pass
10
+
11
+
12
+ class InvalidCurrency(ValidationError):
13
+ pass
14
+
15
+
16
+ class NegativeStockError(ValidationError):
17
+ pass
18
+
19
+
20
+ class InvalidAccountError(ValidationError):
21
+ pass
22
+
23
+
24
+ class ClosedAccountingPeriod(ValidationError):
25
+ pass
26
+
27
+
28
+ class InsufficientFunds(ValidationError):
29
+ pass
30
+
31
+
32
+ class DebitCreditNotEqual(ValidationError):
33
+ pass
34
+
35
+
36
+ class DocumentStatusError(ValidationError):
37
+ pass
lambda_erp/hooks.py ADDED
@@ -0,0 +1,37 @@
1
+ """Lightweight lifecycle hook registry for the core+extension model.
2
+
3
+ A customer deployment (a separate repo depending on this core) registers
4
+ handlers for document lifecycle events without editing core files. Events are
5
+ named ``"<DocType>:<phase>"`` where phase is one of:
6
+
7
+ before_save / after_save
8
+ before_submit / after_submit
9
+ before_cancel / after_cancel
10
+
11
+ Semantics (see docs/core-extension-architecture.md):
12
+ - ``before_*`` fire inside the document's transaction — a raising handler
13
+ aborts and rolls back the operation (use for extra validation / guards).
14
+ - ``after_*`` fire after the change is committed and durable — use for
15
+ side-effects (notifications, external sync). A raising ``after_*`` handler
16
+ propagates but does NOT undo the committed voucher.
17
+ """
18
+
19
+ from collections import defaultdict
20
+
21
+ _HOOKS: dict[str, list] = defaultdict(list)
22
+
23
+
24
+ def register_hook(event: str, fn) -> None:
25
+ """Register a callable ``fn(doc, *args, **kwargs)`` for an event."""
26
+ _HOOKS[event].append(fn)
27
+
28
+
29
+ def run_hooks(event: str, *args, **kwargs) -> None:
30
+ """Invoke every handler registered for ``event``, in registration order."""
31
+ for fn in _HOOKS.get(event, ()): # plain .get avoids creating empty lists
32
+ fn(*args, **kwargs)
33
+
34
+
35
+ def clear_hooks() -> None:
36
+ """Remove all registered handlers (used for test isolation)."""
37
+ _HOOKS.clear()
lambda_erp/model.py ADDED
@@ -0,0 +1,462 @@
1
+ """
2
+ Base Document class replacing the framework's framework.model.document.Document.
3
+
4
+ In the reference implementation, every DocType instance is a Document with lifecycle hooks
5
+ (validate, before_submit, on_submit, on_cancel, etc.), child table support,
6
+ and automatic DB persistence. This module provides the same pattern.
7
+ """
8
+
9
+ import copy
10
+ from lambda_erp.utils import _dict, flt, new_name, now
11
+ from lambda_erp.database import get_db
12
+ from lambda_erp.exceptions import ValidationError, DocumentStatusError
13
+ from lambda_erp.hooks import run_hooks
14
+
15
+
16
+ # Document status constants (mirrors the framework's docstatus)
17
+ DRAFT = 0
18
+ SUBMITTED = 1
19
+ CANCELLED = 2
20
+
21
+
22
+ class Document:
23
+ """Base class for all ERP documents.
24
+
25
+ Mirrors the framework's Document class with:
26
+ - Attribute-style field access
27
+ - Child table support (items, taxes, etc.)
28
+ - Lifecycle hooks (validate, on_submit, on_cancel)
29
+ - Automatic DB persistence
30
+ - Status tracking via docstatus
31
+ """
32
+
33
+ DOCTYPE = None # Override in subclasses, e.g. "Sales Invoice"
34
+ CHILD_TABLES = {} # {"items": ("Sales Invoice Item", SalesInvoiceItem), ...}
35
+ PREFIX = "DOC" # For auto-naming
36
+
37
+ # Master-reference validation — checked on every save(). Each entry is
38
+ # (field_name, master_doctype) at the parent level, or nested per child
39
+ # table key. A typo in e.g. supplier="SUPP-XX5" is caught here rather
40
+ # than silently creating an invoice against a phantom supplier.
41
+ LINK_FIELDS: dict = {} # {field_name: master_doctype}
42
+ CHILD_LINK_FIELDS: dict = {} # {child_key: {field_name: master_doctype}}
43
+
44
+ # Dynamic-link fields — target doctype is determined at runtime by
45
+ # reading a sibling "type" field. Example: Payment Entry's `party` field
46
+ # points at either Customer or Supplier depending on `party_type`.
47
+ # Shape: {field_name: (type_field, {type_value: master_doctype, ...})}
48
+ DYNAMIC_LINK_FIELDS: dict = {}
49
+ CHILD_DYNAMIC_LINK_FIELDS: dict = {} # same shape, nested by child table key
50
+
51
+ # Account-type direction constraints — check root_type / account_type on
52
+ # linked Account fields so a Sales Invoice can't accidentally post its
53
+ # Income to a random Expense account (GL still balances, P&L is junk).
54
+ # Shape: {field_name: {"root_type": str | list, "account_type": str | list}}
55
+ ACCOUNT_TYPE_CONSTRAINTS: dict = {}
56
+ CHILD_ACCOUNT_TYPE_CONSTRAINTS: dict = {}
57
+
58
+ def __init__(self, data=None, **kwargs):
59
+ self._data = _dict(data or {})
60
+ self._data.update(kwargs)
61
+ self._children = {} # field_name -> list of child dicts
62
+
63
+ if not self._data.get("name"):
64
+ self._data["name"] = new_name(self.PREFIX)
65
+ if not self._data.get("docstatus"):
66
+ self._data["docstatus"] = DRAFT
67
+ if not self._data.get("creation"):
68
+ self._data["creation"] = now()
69
+
70
+ # Initialize child tables
71
+ for field_name, (child_doctype, child_cls) in self.CHILD_TABLES.items():
72
+ children = self._data.pop(field_name, []) or []
73
+ self._children[field_name] = []
74
+ for i, child in enumerate(children):
75
+ if isinstance(child, dict):
76
+ child = _dict(child)
77
+ child["parent"] = self._data["name"]
78
+ child["idx"] = child.get("idx", i + 1)
79
+ if not child.get("name"):
80
+ child["name"] = new_name(f"{self.PREFIX}-ITEM")
81
+ self._children[field_name].append(child)
82
+
83
+ # --- Attribute access (mirrors the framework's Document) ---
84
+
85
+ def __getattr__(self, key):
86
+ if key.startswith("_"):
87
+ raise AttributeError(key)
88
+ # Check child tables first
89
+ if key in self.__dict__.get("_children", {}):
90
+ return self._children[key]
91
+ data = self.__dict__.get("_data", {})
92
+ return data.get(key)
93
+
94
+ def __setattr__(self, key, value):
95
+ if key.startswith("_") or key in ("DOCTYPE", "CHILD_TABLES", "PREFIX"):
96
+ super().__setattr__(key, value)
97
+ else:
98
+ self._data[key] = value
99
+
100
+ def __getitem__(self, key):
101
+ if key in self._children:
102
+ return self._children[key]
103
+ return self._data[key]
104
+
105
+ def __setitem__(self, key, value):
106
+ if key in self._children:
107
+ self._children[key] = value
108
+ else:
109
+ self._data[key] = value
110
+
111
+ def __contains__(self, key):
112
+ return key in self._data or key in self._children
113
+
114
+ def get(self, key, default=None):
115
+ if key in self._children:
116
+ return self._children[key]
117
+ return self._data.get(key, default)
118
+
119
+ def set(self, key, value):
120
+ if key in self._children:
121
+ self._children[key] = value
122
+ else:
123
+ self._data[key] = value
124
+
125
+ def update(self, d):
126
+ for key, value in d.items():
127
+ self.set(key, value)
128
+
129
+ def as_dict(self):
130
+ d = _dict(self._data.copy())
131
+ for field_name, children in self._children.items():
132
+ d[field_name] = [_dict(c) if isinstance(c, dict) else c for c in children]
133
+ return d
134
+
135
+ @property
136
+ def docstatus(self):
137
+ return self._data.get("docstatus", DRAFT)
138
+
139
+ # --- Child table helpers ---
140
+
141
+ def append(self, table_name, row=None):
142
+ """Add a row to a child table. Mirrors doc.append('items', {...})."""
143
+ if table_name not in self._children:
144
+ self._children[table_name] = []
145
+
146
+ if row is None:
147
+ row = {}
148
+ if isinstance(row, dict):
149
+ row = _dict(row)
150
+
151
+ row["parent"] = self._data["name"]
152
+ row["idx"] = len(self._children[table_name]) + 1
153
+ if not row.get("name"):
154
+ row["name"] = new_name(f"{self.PREFIX}-ITEM")
155
+
156
+ self._children[table_name].append(row)
157
+ return row
158
+
159
+ # --- Lifecycle hooks (override in subclasses) ---
160
+
161
+ def validate(self):
162
+ """Called before save/submit. Override to add validation logic."""
163
+ pass
164
+
165
+ def before_save(self):
166
+ pass
167
+
168
+ def before_submit(self):
169
+ pass
170
+
171
+ def on_submit(self):
172
+ """Called after submit. Override to post GL entries, update stock, etc."""
173
+ pass
174
+
175
+ def on_cancel(self):
176
+ """Called on cancellation. Override to reverse GL entries, etc."""
177
+ pass
178
+
179
+ def _validate_links(self):
180
+ """Check every declared LINK_FIELDS / CHILD_LINK_FIELDS reference
181
+ actually exists in its master table. Runs after the subclass's own
182
+ validate() so that default values populated by e.g. _set_missing_accounts
183
+ are already in place."""
184
+ db = get_db()
185
+
186
+ def _label(field: str) -> str:
187
+ return field.replace("_", " ").strip().title()
188
+
189
+ def _check(master: str, value, where: str, field: str):
190
+ if not value:
191
+ return
192
+ if not db.exists(master, value):
193
+ raise ValidationError(
194
+ f"{self.DOCTYPE}: {where}{_label(field)} '{value}' does not "
195
+ f"exist in {master}"
196
+ )
197
+
198
+ for field, master in self.LINK_FIELDS.items():
199
+ _check(master, self.get(field), "", field)
200
+
201
+ for child_key, fields in self.CHILD_LINK_FIELDS.items():
202
+ for idx, row in enumerate(self.get(child_key) or [], start=1):
203
+ row_get = row.get if isinstance(row, dict) else (lambda k: getattr(row, k, None))
204
+ for field, master in fields.items():
205
+ _check(master, row_get(field), f"row {idx} ", field)
206
+
207
+ # Dynamic-link fields: resolve the target master from a sibling
208
+ # type field. Empty values are allowed, but once a dynamic-link value
209
+ # is present its discriminator must resolve to a known master.
210
+ for field, (type_field, type_map) in self.DYNAMIC_LINK_FIELDS.items():
211
+ value = self.get(field)
212
+ if not value:
213
+ continue
214
+ type_value = self.get(type_field)
215
+ master = type_map.get(type_value)
216
+ if not master:
217
+ raise ValidationError(
218
+ f"{self.DOCTYPE}: {_label(type_field)} '{type_value}' is not valid for "
219
+ f"{_label(field)}"
220
+ )
221
+ _check(master, value, "", field)
222
+
223
+ for child_key, fields in self.CHILD_DYNAMIC_LINK_FIELDS.items():
224
+ for idx, row in enumerate(self.get(child_key) or [], start=1):
225
+ row_get = row.get if isinstance(row, dict) else (lambda k: getattr(row, k, None))
226
+ for field, (type_field, type_map) in fields.items():
227
+ value = row_get(field)
228
+ if not value:
229
+ continue
230
+ type_value = row_get(type_field)
231
+ master = type_map.get(type_value)
232
+ if not master:
233
+ raise ValidationError(
234
+ f"{self.DOCTYPE}: row {idx} {_label(type_field)} '{type_value}' "
235
+ f"is not valid for {_label(field)}"
236
+ )
237
+ _check(master, value, f"row {idx} ", field)
238
+
239
+ # Account-type direction constraints. Runs after the existence checks
240
+ # above, so we know the account row exists. If a constraint fails the
241
+ # underlying GL would still balance, but reports (P&L especially)
242
+ # would be nonsense — e.g. revenue posted to Administrative Expenses.
243
+ def _check_account(account: str, constraint: dict, where: str, field: str):
244
+ info = db.get_value("Account", account, ["root_type", "account_type"])
245
+ if not info:
246
+ return # link check already caught nonexistent account
247
+ for key, allowed in constraint.items():
248
+ actual = info.get(key)
249
+ allowed_set = {allowed} if isinstance(allowed, str) else set(allowed)
250
+ if actual not in allowed_set:
251
+ expected = " or ".join(sorted(allowed_set))
252
+ raise ValidationError(
253
+ f"{self.DOCTYPE}: {where}{_label(field)} '{account}' has "
254
+ f"{key}={actual!r}, expected {expected}"
255
+ )
256
+
257
+ for field, constraint in self.ACCOUNT_TYPE_CONSTRAINTS.items():
258
+ value = self.get(field)
259
+ if value:
260
+ _check_account(value, constraint, "", field)
261
+
262
+ for child_key, fields in self.CHILD_ACCOUNT_TYPE_CONSTRAINTS.items():
263
+ for idx, row in enumerate(self.get(child_key) or [], start=1):
264
+ row_get = row.get if isinstance(row, dict) else (lambda k: getattr(row, k, None))
265
+ for field, constraint in fields.items():
266
+ value = row_get(field)
267
+ if value:
268
+ _check_account(value, constraint, f"row {idx} ", field)
269
+
270
+ # --- Persistence ---
271
+
272
+ def save(self):
273
+ """Validate and save to database.
274
+
275
+ Only drafts can be saved. Submitted documents are immutable by design
276
+ — re-running validate() on a submitted doc would recompute totals and
277
+ reset outstanding_amount, silently decoupling the subledger (aging,
278
+ outstanding) from the already-posted GL. Post-submit mutations that
279
+ are genuinely needed (outstanding_amount, billed_qty, modified) go
280
+ through db.set_value directly rather than round-tripping save().
281
+ """
282
+ if self.docstatus != DRAFT:
283
+ raise DocumentStatusError(
284
+ f"Cannot save {self.DOCTYPE} {self.name}: document is "
285
+ f"{'submitted' if self.docstatus == SUBMITTED else 'cancelled'}. "
286
+ f"Submitted docs are immutable; cancel and create a new one to amend."
287
+ )
288
+ self._data["modified"] = now()
289
+ self.validate()
290
+ self._validate_links()
291
+ self.before_save()
292
+ run_hooks(f"{self.DOCTYPE}:before_save", self)
293
+ self._persist()
294
+ run_hooks(f"{self.DOCTYPE}:after_save", self)
295
+ return self
296
+
297
+ def submit(self):
298
+ """Submit the document (docstatus 0 -> 1).
299
+
300
+ In the reference implementation, submitting a document is what actually posts GL entries,
301
+ creates stock ledger entries, etc. Draft documents have no effect
302
+ on the ledgers.
303
+
304
+ The entire operation (docstatus change + on_submit hooks like GL/stock
305
+ posting) is wrapped in a transaction. If on_submit() fails, the
306
+ docstatus change is rolled back.
307
+ """
308
+ if self.docstatus != DRAFT:
309
+ raise DocumentStatusError(
310
+ f"Cannot submit {self.DOCTYPE} {self.name}: docstatus is {self.docstatus}"
311
+ )
312
+
313
+ db = get_db()
314
+ db._in_transaction = True
315
+ self._data["modified"] = now()
316
+ self.validate()
317
+ self.before_submit()
318
+ self._data["docstatus"] = SUBMITTED
319
+ self._data["status"] = "Submitted"
320
+ try:
321
+ # Inside the transaction: a raising before_submit hook aborts and
322
+ # rolls back the whole submit (use for guards / extra validation).
323
+ run_hooks(f"{self.DOCTYPE}:before_submit", self)
324
+ self._persist(commit=False)
325
+ self.on_submit()
326
+ db.commit()
327
+ except Exception:
328
+ db.conn.rollback()
329
+ # Restore in-memory state
330
+ self._data["docstatus"] = DRAFT
331
+ self._data["status"] = "Draft"
332
+ raise
333
+ finally:
334
+ db._in_transaction = False
335
+ # Post-commit: the voucher is durable. after_submit is for side-effects
336
+ # (notifications, external sync); a raise here does NOT undo the submit.
337
+ run_hooks(f"{self.DOCTYPE}:after_submit", self)
338
+ return self
339
+
340
+ def cancel(self):
341
+ """Cancel a submitted document (docstatus 1 -> 2).
342
+
343
+ Wrapped in a transaction — if on_cancel() fails (e.g. reversing
344
+ GL entries), the docstatus change is rolled back.
345
+ """
346
+ if self.docstatus != SUBMITTED:
347
+ raise DocumentStatusError(
348
+ f"Cannot cancel {self.DOCTYPE} {self.name}: docstatus is {self.docstatus}"
349
+ )
350
+
351
+ db = get_db()
352
+ db._in_transaction = True
353
+ self._data["docstatus"] = CANCELLED
354
+ self._data["status"] = "Cancelled"
355
+ self._data["modified"] = now()
356
+ try:
357
+ run_hooks(f"{self.DOCTYPE}:before_cancel", self)
358
+ self._persist(commit=False)
359
+ self.on_cancel()
360
+ db.commit()
361
+ except Exception:
362
+ db.conn.rollback()
363
+ self._data["docstatus"] = SUBMITTED
364
+ self._data["status"] = "Submitted"
365
+ raise
366
+ finally:
367
+ db._in_transaction = False
368
+ run_hooks(f"{self.DOCTYPE}:after_cancel", self)
369
+ return self
370
+
371
+ def _persist(self, commit=True):
372
+ """Save document and child tables to database."""
373
+ db = get_db()
374
+ doctype = self.DOCTYPE
375
+
376
+ # Build a clean dict with only the parent-level fields
377
+ parent_data = {}
378
+ for key, value in self._data.items():
379
+ if key not in self._children:
380
+ parent_data[key] = value
381
+
382
+ # Upsert parent (only persist columns that exist in the table)
383
+ valid_columns = db._get_table_columns(doctype)
384
+ filtered_data = {k: v for k, v in parent_data.items() if k in valid_columns}
385
+
386
+ if db.exists(doctype, self._data["name"]):
387
+ sets = ", ".join(f'"{k}" = ?' for k in filtered_data if k != "name")
388
+ params = [v for k, v in filtered_data.items() if k != "name"]
389
+ params.append(filtered_data["name"])
390
+ db.conn.execute(f'UPDATE "{doctype}" SET {sets} WHERE name = ?', params)
391
+ else:
392
+ db.insert(doctype, filtered_data)
393
+
394
+ # Persist child tables
395
+ for field_name, (child_doctype, child_cls) in self.CHILD_TABLES.items():
396
+ # Delete existing children and re-insert
397
+ db.delete(child_doctype, filters={"parent": self._data["name"]})
398
+ for child in self._children.get(field_name, []):
399
+ child_data = dict(child) if isinstance(child, dict) else child
400
+ db.insert(child_doctype, child_data)
401
+
402
+ if commit:
403
+ db.commit()
404
+
405
+ def reload(self):
406
+ """Reload from database."""
407
+ db = get_db()
408
+ rows = db.get_all(self.DOCTYPE, filters={"name": self.name}, fields=["*"])
409
+ if rows:
410
+ self._data = rows[0]
411
+ for field_name, (child_doctype, child_cls) in self.CHILD_TABLES.items():
412
+ children = db.get_all(
413
+ child_doctype,
414
+ filters={"parent": self.name},
415
+ fields=["*"],
416
+ order_by="idx",
417
+ )
418
+ self._children[field_name] = children
419
+
420
+ @classmethod
421
+ def load(cls, name):
422
+ """Load a document from the database by name."""
423
+ db = get_db()
424
+ rows = db.get_all(cls.DOCTYPE, filters={"name": name}, fields=["*"])
425
+ if not rows:
426
+ raise ValidationError(f"{cls.DOCTYPE} {name} not found")
427
+
428
+ doc = cls(rows[0])
429
+ for field_name, (child_doctype, child_cls) in cls.CHILD_TABLES.items():
430
+ children = db.get_all(
431
+ child_doctype,
432
+ filters={"parent": name},
433
+ fields=["*"],
434
+ order_by="idx",
435
+ )
436
+ doc._children[field_name] = children
437
+ return doc
438
+
439
+ def __repr__(self):
440
+ return f"<{self.DOCTYPE}: {self.name}>"
441
+
442
+ # --- Utility methods used by controllers ---
443
+
444
+ def precision(self, fieldname):
445
+ """Return precision for a field. Default 2 for currency fields."""
446
+ # Simplified - the reference implementation pulls this from DocType metadata
447
+ return 2
448
+
449
+ def round_floats_in(self, doc=None, do_not_round_fields=None):
450
+ """Round float values in the document."""
451
+ if doc is None:
452
+ doc = self._data
453
+ if isinstance(doc, dict):
454
+ target = doc
455
+ else:
456
+ target = doc._data if hasattr(doc, "_data") else doc
457
+
458
+ for key, value in list(target.items()):
459
+ if isinstance(value, float) and (
460
+ not do_not_round_fields or key not in do_not_round_fields
461
+ ):
462
+ target[key] = flt(value, 2)
File without changes