lambda-erp 1.1.8__tar.gz → 1.1.9__tar.gz

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 (114) hide show
  1. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/PKG-INFO +1 -1
  2. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/pdf.py +7 -3
  3. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/pdf_contract.py +29 -3
  4. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/templates/_record_base.html +1 -1
  5. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/templates/document.html +2 -1
  6. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/templates/proposal.html +3 -2
  7. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/pyproject.toml +1 -1
  8. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/.gitignore +0 -0
  9. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/LICENSE +0 -0
  10. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/README.md +0 -0
  11. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/__init__.py +0 -0
  12. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/attachments.py +0 -0
  13. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/auth.py +0 -0
  14. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/bootstrap.py +0 -0
  15. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/chat.py +0 -0
  16. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/chat_links.py +0 -0
  17. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/demo_limits.py +0 -0
  18. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/deps.py +0 -0
  19. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/erp_context.py +0 -0
  20. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/errors.py +0 -0
  21. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/list_values.py +0 -0
  22. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/main.py +0 -0
  23. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/mcp_oauth.py +0 -0
  24. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/oauth.py +0 -0
  25. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/pdf_exports.py +0 -0
  26. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/pdf_profiles.py +0 -0
  27. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/providers.py +0 -0
  28. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/remarks_md.py +0 -0
  29. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/__init__.py +0 -0
  30. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/accounting.py +0 -0
  31. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/actions.py +0 -0
  32. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/admin.py +0 -0
  33. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/analytics.py +0 -0
  34. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/availability.py +0 -0
  35. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/bank_reconciliation.py +0 -0
  36. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/bank_statements.py +0 -0
  37. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/chat_api.py +0 -0
  38. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/documents.py +0 -0
  39. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/masters.py +0 -0
  40. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/mcp.py +0 -0
  41. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/proposals.py +0 -0
  42. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/reports.py +0 -0
  43. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/routers/setup.py +0 -0
  44. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/services.py +0 -0
  45. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/templates/_proposal_totals.html +0 -0
  46. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/templates/record.html +0 -0
  47. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/time_filters.py +0 -0
  48. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/api/tool_permissions.py +0 -0
  49. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/docs/agents/README.md +0 -0
  50. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/frontend/LICENSE +0 -0
  51. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/frontend/README.md +0 -0
  52. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/frontend/src/api/client.ts +0 -0
  53. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/__init__.py +0 -0
  54. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/__init__.py +0 -0
  55. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/bank_account.py +0 -0
  56. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/bank_reconciliation.py +0 -0
  57. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/bank_statement_import.py +0 -0
  58. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/bank_transaction.py +0 -0
  59. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/budget.py +0 -0
  60. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/camt.py +0 -0
  61. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  62. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/general_ledger.py +0 -0
  63. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/journal_entry.py +0 -0
  64. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/payment_entry.py +0 -0
  65. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/pos_invoice.py +0 -0
  66. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/purchase_invoice.py +0 -0
  67. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/revaluation.py +0 -0
  68. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/sales_invoice.py +0 -0
  69. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/settlement.py +0 -0
  70. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/__init__.py +0 -0
  71. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/engine.py +0 -0
  72. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/pack.py +0 -0
  73. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  74. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  75. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  76. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  77. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  78. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  79. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/profiles.py +0 -0
  80. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/setup/spine.py +0 -0
  81. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/accounting/subscription.py +0 -0
  82. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/assets/__init__.py +0 -0
  83. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/assets/asset.py +0 -0
  84. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/assets/lifecycle.py +0 -0
  85. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/assets/reservation.py +0 -0
  86. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/buying/__init__.py +0 -0
  87. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/buying/purchase_order.py +0 -0
  88. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/controllers/__init__.py +0 -0
  89. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/controllers/currency.py +0 -0
  90. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/controllers/defaults.py +0 -0
  91. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/controllers/item_price.py +0 -0
  92. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/controllers/item_prices.py +0 -0
  93. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/controllers/pricing_rule.py +0 -0
  94. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  95. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/database.py +0 -0
  96. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/exceptions.py +0 -0
  97. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/hooks.py +0 -0
  98. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/master_integrity.py +0 -0
  99. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/model.py +0 -0
  100. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/selling/__init__.py +0 -0
  101. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/selling/proposal.py +0 -0
  102. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/selling/quotation.py +0 -0
  103. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/selling/sales_order.py +0 -0
  104. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/simulation.py +0 -0
  105. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/stock/__init__.py +0 -0
  106. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/stock/delivery_note.py +0 -0
  107. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/stock/purchase_receipt.py +0 -0
  108. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/stock/stock_entry.py +0 -0
  109. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/stock/stock_ledger.py +0 -0
  110. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/timestamps.py +0 -0
  111. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/utils.py +0 -0
  112. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/validation.py +0 -0
  113. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/lambda_erp/workflow.py +0 -0
  114. {lambda_erp-1.1.8 → lambda_erp-1.1.9}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 1.1.8
3
+ Version: 1.1.9
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -7,7 +7,7 @@ from jinja2 import Environment, FileSystemLoader, ChoiceLoader
7
7
  from weasyprint import HTML
8
8
  from lambda_erp.database import get_db
9
9
  from api.services import load_document, get_document_class
10
- from api.pdf_contract import prepare, validate_rendered_pdf, PDFError
10
+ from api.pdf_contract import prepare, validate_rendered_pdf, PDFError, register_pdf_state_labels # noqa: F401
11
11
  from api.remarks_md import render_remarks
12
12
  from lambda_erp.controllers.taxes_and_totals import split_by_frequency
13
13
 
@@ -229,6 +229,7 @@ def _render_commercial_pdf(doctype_slug, name, doc, contract):
229
229
  context = dict(
230
230
  doc=doc,
231
231
  pdf_status=contract["status"],
232
+ pdf_state_notice=contract["notice"],
232
233
  pdf_warnings=contract["warnings"],
233
234
  title=title,
234
235
  company_name=company_name,
@@ -334,8 +335,8 @@ def _render_proposal_pdf(name, proposal, contract):
334
335
  if quote.get('company') != proposal.get('company') or quote.get('customer') != proposal.get('customer'):
335
336
  raise PDFError('Proposal PDF: quotation company and customer must match')
336
337
  quote_contract = prepare('Quotation', quote)
337
- # Each included offer must be rendered completely. Its state is printed
338
- # alongside its number, including historical cancelled/voided offers.
338
+ # Each included offer must be rendered completely. A non-final state
339
+ # (draft, cancelled, discarded) is printed beside its number.
339
340
  contract['tokens'].extend(quote_contract['tokens'])
340
341
  contract['warnings'].extend(quote_contract['warnings'])
341
342
  items = quote.get("items", []) or []
@@ -352,6 +353,7 @@ def _render_proposal_pdf(name, proposal, contract):
352
353
  "letter": chr(ord("A") + i),
353
354
  "quotation": qname,
354
355
  "pdf_status": quote_contract["status"],
356
+ "pdf_state_notice": quote_contract["notice"],
355
357
  "title": row.get("position_title") or default_title or qname,
356
358
  "blurb": row.get("position_blurb") or quote.get("remarks") or "",
357
359
  "is_recommended": bool(row.get("is_recommended")),
@@ -373,6 +375,7 @@ def _render_proposal_pdf(name, proposal, contract):
373
375
  context = dict(
374
376
  proposal=proposal,
375
377
  pdf_status=contract["status"],
378
+ pdf_state_notice=contract["notice"],
376
379
  pdf_warnings=contract["warnings"],
377
380
  title=proposal.get("title") or "Offerte",
378
381
  company_name=company_name,
@@ -416,6 +419,7 @@ def _render_record_pdf(doctype, name, doc, contract):
416
419
  page_size_row = get_db().sql('SELECT value FROM "Settings" WHERE key = ?', ['pdf_page_size'])
417
420
  context = dict(
418
421
  title=contract['profile'].title, name=name, pdf_status=contract['status'],
422
+ pdf_state_notice=contract['notice'],
419
423
  pdf_warnings=contract['warnings'], print_fields=contract['fields'],
420
424
  print_tables=contract['tables'], print_labels={}, print_title=contract['profile'].title,
421
425
  company_info=company, company_name=company.get('company_name') or doc.get('company') or '',
@@ -61,6 +61,31 @@ def resolve(table, name):
61
61
  return (db.get_value(table, name, display) if display in fields else None) or name
62
62
 
63
63
 
64
+ # States a reader must not mistake for a final, issued document. Only these are
65
+ # printed as a notice and required in the rendered text; a submitted invoice or
66
+ # an ordinary record status carries no notice.
67
+ NOTICE_STATES = ('Draft', 'Cancelled', 'Discarded')
68
+ _state_labels = {}
69
+
70
+
71
+ def register_pdf_state_labels(labels) -> None:
72
+ """Override the printed notice for non-final states, e.g. {'Draft': 'Entwurf'}.
73
+
74
+ Keys must be in NOTICE_STATES. The label is what templates print as
75
+ `pdf_state_notice` and what the rendered-text check then requires.
76
+ """
77
+ unknown = set(labels) - set(NOTICE_STATES)
78
+ if unknown:
79
+ raise ValueError(f'Unknown PDF notice states: {sorted(unknown)}')
80
+ if any(not str(label).strip() for label in labels.values()):
81
+ raise ValueError('PDF notice labels must not be empty')
82
+ _state_labels.update(labels)
83
+
84
+
85
+ def notice_for(state):
86
+ return _state_labels.get(state, state) if state in NOTICE_STATES else ''
87
+
88
+
64
89
  def state_for(doctype, doc):
65
90
  from api.services import DOCUMENT_CLASSES
66
91
  if doc.get('discarded'):
@@ -87,9 +112,10 @@ def prepare(doctype, doc):
87
112
  if not any(not missing(doc.get(key)) for key in group):
88
113
  raise PDFError('PDF requires at least one of: ' + ', '.join(group), fields=group)
89
114
  state = state_for(doctype, doc)
90
- tokens = [str(doc['name']), state]
115
+ notice = notice_for(state)
116
+ tokens = [str(doc['name'])] + ([notice] if notice else [])
91
117
  warnings = []
92
- if state in {'Draft', 'Cancelled', 'Discarded'}:
118
+ if notice:
93
119
  warnings.append({'code': 'pdf_document_state', 'message': f'{doctype} {doc["name"]}: {state}'})
94
120
  company = {}
95
121
  if doc.get('company'):
@@ -213,7 +239,7 @@ def prepare(doctype, doc):
213
239
  if doctype == 'Subscription':
214
240
  party_currency = get_db().get_value(party_type, party_id, 'default_currency') if party_id else None
215
241
  fields.append({'key': 'currency', 'value': party_currency or company_currency})
216
- return {'profile': profile, 'status': state, 'warnings': warnings, 'tokens': tokens,
242
+ return {'profile': profile, 'status': state, 'notice': notice, 'warnings': warnings, 'tokens': tokens,
217
243
  'fields': fields, 'tables': tables, 'party_name': party_name,
218
244
  'company': company, 'company_currency': company_currency}
219
245
 
@@ -16,7 +16,7 @@ tr { break-inside: avoid; } .brand { font-weight: bold; letter-spacing: .8pt; }
16
16
  {% macro label(key) %}{{ print_labels.get(key, key.replace('_',' ').capitalize()) }}{% endmacro %}
17
17
  {% block letterhead %}<div class="header"><div class="brand">{{ company_name }}</div>{% if company_info.get('address') %}<div class="muted">{{ company_info.get('address') }}</div>{% endif %}</div>{% endblock %}
18
18
  <h1>{{ print_title }}</h1>
19
- <div class="state">{{ name }} · {{ pdf_status }}</div>
19
+ <div class="state">{{ name }} · {{ pdf_state_notice or pdf_status }}</div>
20
20
  {% for warning in pdf_warnings %}<div class="warning">{{ warning.message }}</div>{% endfor %}
21
21
  {% if party_name %}<p><strong>{{ party_name }}</strong></p>{% endif %}
22
22
  {% if company_currency %}<div class="muted">{{ label('company_currency') }}: {{ company_currency }}</div>{% endif %}
@@ -58,10 +58,11 @@
58
58
  .remarks .rm-hr { border-top: 1px solid #d1d5db; margin: 6px 0; }
59
59
  .remarks em { font-style: italic; }
60
60
  .remarks strong { font-weight: 700; }
61
+ .state-notice { display: inline-block; margin-bottom: 8pt; padding: 3px 10px; border: 1.5px solid #b91c1c; border-radius: 4px; color: #b91c1c; font-size: 10pt; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; }
61
62
  </style>
62
63
  </head>
63
64
  <body>
64
- <div style="font-size:9pt; margin-bottom:8pt">{{ pdf_status }}</div>
65
+ {% if pdf_state_notice %}<div class="state-notice">{{ pdf_state_notice }}</div>{% endif %}
65
66
 
66
67
  {# ---- Header ---- #}
67
68
  <div class="header">
@@ -34,10 +34,11 @@
34
34
  table.lines td.desc { color: #333; }
35
35
 
36
36
  .footer { margin-top: 28px; padding-top: 10px; border-top: 1px solid #e5e7eb; font-size: 7.5pt; color: #999; text-align: center; }
37
+ .state-notice { display: inline-block; margin-bottom: 8pt; padding: 3px 10px; border: 1.5px solid #b91c1c; border-radius: 4px; color: #b91c1c; font-size: 10pt; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; }
37
38
  </style>
38
39
  </head>
39
40
  <body>
40
- <div style="font-size:9pt; margin-bottom:8pt">{{ pdf_status }}</div>
41
+ {% if pdf_state_notice %}<div class="state-notice">{{ pdf_state_notice }}</div>{% endif %}
41
42
 
42
43
  <div class="header">
43
44
  <div><div class="company">{{ company_name or proposal.company or "" }}</div></div>
@@ -69,7 +70,7 @@
69
70
  {% if proposal.cover_letter %}<div class="cover">{{ proposal.cover_letter }}</div>{% endif %}
70
71
 
71
72
  {% for p in positions %}
72
- <div style="font-size:8pt">{{ p.quotation }} · {{ p.pdf_status }}</div>
73
+ {% if p.pdf_state_notice %}<div style="font-size:8pt">{{ p.quotation }} · <span class="state-notice" style="font-size:8pt; padding:1px 6px; margin:0">{{ p.pdf_state_notice }}</span></div>{% endif %}
73
74
  <div class="position">
74
75
  <div class="pos-head">
75
76
  <span class="letter">{{ p.letter }}</span>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "1.1.8"
3
+ version = "1.1.9"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes