lambda-erp 1.2.0__py3-none-any.whl → 1.2.2__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.
api/chat.py CHANGED
@@ -3739,7 +3739,8 @@ async def run_thinking_loop(
3739
3739
  success = False
3740
3740
  else:
3741
3741
  try:
3742
- result = await asyncio.to_thread(handler, fn_args)
3742
+ from api.write_context import run_as_actor
3743
+ result = await asyncio.to_thread(run_as_actor, principal_ref[0], handler, fn_args)
3743
3744
  success = not (isinstance(result, dict) and ("error" in result or result.get("ok") is False or result.get("failed", 0)))
3744
3745
  except Exception as e:
3745
3746
  result = {"error": str(e)}
api/mentions.py CHANGED
@@ -1,11 +1,13 @@
1
- """Personal mentions with explicit recipients, atomic delivery and plugin sources.
1
+ """Personal mentions resolved from visible note text, with atomic delivery.
2
2
 
3
- No email/push delivery, inferred recipients, or caller-selectable inbox owner.
3
+ No email/push delivery, hidden recipients, or caller-selectable inbox owner.
4
4
  Plugins own record access and note storage; this module owns notifications.
5
5
  """
6
6
  import hashlib
7
7
  import json
8
+ import re
8
9
  import uuid
10
+ from contextvars import ContextVar
9
11
 
10
12
  from fastapi import HTTPException
11
13
  from pydantic import BaseModel, ConfigDict, Field
@@ -17,6 +19,50 @@ from lambda_erp.utils import now
17
19
 
18
20
  SOURCES = {}
19
21
  REAL_ROLES = {"viewer", "manager", "admin"}
22
+ _WRITING_MENTION_NOTE = ContextVar("writing_mention_note", default=False)
23
+ _MENTION_START = re.compile(r"(?<![\w@./+\-])@(?=\S)")
24
+
25
+
26
+ def writing_mention_note():
27
+ """Dedicated note action will deliver after its source has saved the note."""
28
+ return _WRITING_MENTION_NOTE.get()
29
+
30
+
31
+ def _mention_users():
32
+ return get_db().sql('SELECT name, full_name, email, role, enabled FROM "User" '
33
+ "WHERE enabled = 1 AND role IN ('viewer','manager','admin')")
34
+
35
+
36
+ def _aliases(users):
37
+ aliases = {}
38
+ for user in users:
39
+ for value in (user.get("full_name"), user.get("email")):
40
+ if value and value.strip():
41
+ aliases.setdefault(value.strip().lower(), {})[user["name"]] = user
42
+ return aliases
43
+
44
+
45
+ def resolve_note_mentions(body):
46
+ """Resolve visible @Full Name / @email tokens, never hidden recipient IDs."""
47
+ aliases = _aliases(_mention_users())
48
+ patterns = [(re.compile(re.escape(alias) + r"(?![\w@\-])", re.IGNORECASE), users)
49
+ for alias, users in sorted(aliases.items(), key=lambda item: len(item[0]), reverse=True)]
50
+ recipients, end = {}, 0
51
+ for marker in _MENTION_START.finditer(body):
52
+ if marker.start() < end:
53
+ continue # The second @ inside an explicit @email address.
54
+ for pattern, users in patterns:
55
+ match = pattern.match(body, marker.end())
56
+ if match:
57
+ if len(users) != 1:
58
+ raise ValueError("An @mention matches multiple colleagues. Use the person's @email or select them from the suggestions. No note was saved.")
59
+ recipients.update(users)
60
+ end = match.end()
61
+ break
62
+ # Unknown handles are ordinary text, just like unmarked emails.
63
+ if len(recipients) > 20:
64
+ raise ValueError("A note can mention at most 20 colleagues. No note was saved.")
65
+ return recipients
20
66
 
21
67
 
22
68
  def register_mention_source(doctype, *, label, can_access, create_note, resolve_note, resolve_notes=None):
@@ -45,7 +91,6 @@ class Note(Input):
45
91
  doctype: str = Field(min_length=1, max_length=100)
46
92
  record_id: str = Field(min_length=1, max_length=200)
47
93
  body: str = Field(min_length=1, max_length=8000)
48
- mention_user_ids: list[str] = Field(default_factory=list, max_length=20)
49
94
  request_id: str = Field(min_length=8, max_length=100)
50
95
 
51
96
 
@@ -61,6 +106,8 @@ class State(Input):
61
106
 
62
107
 
63
108
  def _actor(user):
109
+ if (user or {}).get('_shared_connection'):
110
+ raise PermissionError("Mentions require a personal ERP connection. The shared team key identifies its owner, not the current user.")
64
111
  from api.auth import refresh_auth_principal
65
112
  actor = refresh_auth_principal(user)
66
113
  if not actor or actor.get("role") not in REAL_ROLES:
@@ -88,6 +135,11 @@ def search_mention_users(args, user):
88
135
  "AND role IN ('viewer','manager','admin') "
89
136
  "AND (LOWER(COALESCE(full_name,'')) LIKE ? ESCAPE '!' OR LOWER(email) LIKE ? ESCAPE '!') "
90
137
  'ORDER BY full_name, name LIMIT 20', [f"%{q}%", f"%{q}%"])
138
+ aliases = _aliases(_mention_users())
139
+ for row in rows:
140
+ full_name = (row.get("full_name") or "").strip()
141
+ token = full_name if full_name and len(aliases.get(full_name.lower(), {})) == 1 else row["email"]
142
+ row["mention_text"] = "@" + token
91
143
  return {"users": rows, "acting_user": _identity(actor),
92
144
  "sources": [{"doctype": key, "label": value["label"]} for key, value in SOURCES.items()]}
93
145
 
@@ -100,8 +152,7 @@ def add_record_note(args, user):
100
152
  raise ValueError("Note body must not be blank.")
101
153
  source, db = _source(data.doctype), get_db()
102
154
  actor_id = _identity(actor)["user_id"]
103
- recipients = sorted(set(data.mention_user_ids))
104
- payload = {**data.model_dump(exclude={"request_id"}), "mention_user_ids": recipients}
155
+ payload = data.model_dump(exclude={"request_id"})
105
156
  digest = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
106
157
  request_pk = uuid.uuid4().hex
107
158
  with db.atomic():
@@ -121,22 +172,13 @@ def add_record_note(args, user):
121
172
  notified_count = db.sql('SELECT COUNT(*) AS n FROM "Mention Notification" WHERE doctype = ? AND note_id = ?',
122
173
  [data.doctype, note_id])[0]["n"]
123
174
  else:
124
- targets = []
125
- for recipient_id in recipients:
126
- recipient = db.get_value("User", recipient_id, ["name", "full_name", "email", "role", "enabled"])
127
- if (not recipient or not recipient.get("enabled") or recipient.get("role") not in REAL_ROLES
128
- or not source["can_access"](data.record_id, dict(recipient))):
129
- raise ValueError("A mentioned colleague cannot access this record. No note was saved.")
130
- if recipient_id != actor_id:
131
- targets.append(recipient_id)
132
- note_id = source["create_note"](data.record_id, data.body.strip(), actor)
175
+ token = _WRITING_MENTION_NOTE.set(True)
176
+ try:
177
+ note_id = source["create_note"](data.record_id, data.body.strip(), actor)
178
+ finally:
179
+ _WRITING_MENTION_NOTE.reset(token)
133
180
  db.sql('UPDATE "Mention Request" SET note_id = ? WHERE id = ?', [note_id, request_pk])
134
- for recipient_id in targets:
135
- db.insert("Mention Notification", {
136
- "id": uuid.uuid4().hex, "recipient_id": recipient_id, "actor_id": actor_id,
137
- "doctype": data.doctype, "note_id": note_id, "created_at": now(),
138
- })
139
- notified_count = len(targets)
181
+ notified_count = sync_note_mentions(data.doctype, data.record_id, note_id, data.body, actor)
140
182
  note = source["resolve_note"](note_id, actor)
141
183
  if not note:
142
184
  raise HTTPException(404, "Note is no longer available")
@@ -144,6 +186,40 @@ def add_record_note(args, user):
144
186
  "notified_count": notified_count, "replayed": repeated, "acting_user": _identity(actor)}
145
187
 
146
188
 
189
+ def sync_note_mentions(doctype, record_id, note_id, body, user):
190
+ """Shared delivery for dedicated actions and generic document save hooks.
191
+
192
+ Call inside the note's save transaction. Existing recipients keep their
193
+ read/done state; new recipients receive one notification; removing a
194
+ visible mention removes its notification. No caller-selected recipient IDs.
195
+ """
196
+ db, source = get_db(), _source(doctype)
197
+ recipients = resolve_note_mentions(body)
198
+ with db.atomic():
199
+ existing = db.sql('SELECT recipient_id FROM "Mention Notification" WHERE doctype = ? AND note_id = ?',
200
+ [doctype, note_id])
201
+ if not recipients and not existing:
202
+ return 0 # Ordinary text needs no personal notification identity.
203
+ actor = _actor(user)
204
+ if actor['role'] not in {'manager', 'admin'}:
205
+ raise PermissionError('Manager access is required to write mention notes.')
206
+ if not source['can_access'](record_id, actor):
207
+ raise HTTPException(404, 'Record is unavailable')
208
+ for recipient in recipients.values():
209
+ if not source['can_access'](record_id, recipient):
210
+ raise ValueError('A mentioned colleague cannot access this record. No note was saved.')
211
+ previous = {row['recipient_id'] for row in existing}
212
+ for recipient_id in previous - recipients.keys():
213
+ db.sql('DELETE FROM "Mention Notification" WHERE doctype = ? AND note_id = ? AND recipient_id = ?',
214
+ [doctype, note_id, recipient_id])
215
+ for recipient_id in recipients.keys() - previous:
216
+ db.insert('Mention Notification', {
217
+ 'id': uuid.uuid4().hex, 'recipient_id': recipient_id, 'actor_id': _identity(actor)['user_id'],
218
+ 'doctype': doctype, 'note_id': note_id, 'created_at': now(),
219
+ })
220
+ return len(recipients)
221
+
222
+
147
223
  def _visible(row, actor, notes=None, authors=None):
148
224
  source = SOURCES.get(row["doctype"])
149
225
  note = (notes.get((row["doctype"], row["note_id"])) if notes is not None else
@@ -223,9 +299,9 @@ def initialize_mentions():
223
299
  db.sql('CREATE INDEX IF NOT EXISTS mention_inbox ON "Mention Notification" (recipient_id, created_at)')
224
300
  for name, handler, schema, role, description in (
225
301
  ("search_mention_users", search_mention_users, Search, "viewer",
226
- "Find enabled ERP colleagues for explicit @mentions by name/email. Returns canonical user IDs, acting_user and supported record types. Do not guess a person when ambiguous."),
302
+ "Find enabled ERP colleagues for visible @mentions by name/email. Returns mention_text: insert that exact text in the note body to tag the person. Also returns canonical user IDs, acting_user and supported record types. Do not guess a person when ambiguous."),
227
303
  ("add_record_note", add_record_note, Note, "manager",
228
- "Save an internal note on a supported record and notify explicitly selected colleagues. Resolve mention_user_ids using search_mention_users. Use a new request_id (UUID) per intended note; reuse it unchanged on retries. No email is sent. Authorship is the authenticated ERP account. Self-mentions do not notify."),
304
+ "Save an internal note and notify people visibly @mentioned in its body, including yourself. First use search_mention_users, then include each recipient's exact mention_text in body, e.g. 'salü vom johnny @Jon Torus'. Recipients are determined only from body; there is no recipient-ID argument. Unknown @handles and ordinary emails remain text. Ambiguous names require @email. Generic CRM note writes use the same mention processing. Use a new request_id (UUID) per intended note; reuse it unchanged on retries. No email is sent. Authorship is the authenticated ERP account."),
229
305
  ("list_my_mentions", list_my_mentions, Inbox, "viewer",
230
306
  "List the authenticated ERP user's mentions, with record/note links. open means not done; unread is separate. Use this for 'my mentions', never guess or pass another user. Returns acting_user, unread_count and pagination."),
231
307
  ("set_mention_state", set_mention_state, State, "viewer",
api/routers/documents.py CHANGED
@@ -264,12 +264,14 @@ def get_doc(doctype_slug: str, name: str, _user: dict = _viewer):
264
264
 
265
265
  @router.post("/{doctype_slug}")
266
266
  def create_doc(doctype_slug: str, data: dict, _user: dict = _manager):
267
- return create_document(doctype_slug, data)
267
+ from api.write_context import run_as_actor
268
+ return run_as_actor(_user, create_document, doctype_slug, data)
268
269
 
269
270
 
270
271
  @router.put("/{doctype_slug}/{name}")
271
272
  def update_doc(doctype_slug: str, name: str, data: dict, _user: dict = _manager):
272
- return update_document(doctype_slug, name, data)
273
+ from api.write_context import run_as_actor
274
+ return run_as_actor(_user, update_document, doctype_slug, name, data)
273
275
 
274
276
 
275
277
  @router.post("/{doctype_slug}/{name}/submit")
api/routers/masters.py CHANGED
@@ -720,12 +720,14 @@ def get_master(master_type: str, name: str, _user: dict = _viewer):
720
720
 
721
721
  @router.post("/{master_type}")
722
722
  def create_master(master_type: str, data: dict, _user: dict = _manager):
723
- return create_master_record(master_type, data)
723
+ from api.write_context import run_as_actor
724
+ return run_as_actor(_user, create_master_record, master_type, data)
724
725
 
725
726
 
726
727
  @router.put("/{master_type}/{name}")
727
728
  def update_master(master_type: str, name: str, data: dict, _user: dict = _manager):
728
- return update_master_record(master_type, name, data)
729
+ from api.write_context import run_as_actor
730
+ return run_as_actor(_user, update_master_record, master_type, name, data)
729
731
 
730
732
 
731
733
  def delete_master_record(master_type: str, name: str) -> dict:
api/routers/mcp.py CHANGED
@@ -139,7 +139,8 @@ def _call(name: str, args: dict, user: dict):
139
139
  handler = handlers.get(name)
140
140
  if handler is None:
141
141
  raise KeyError(name)
142
- return handler(args or {})
142
+ from api.write_context import run_as_actor
143
+ return run_as_actor(user, handler, args or {})
143
144
 
144
145
 
145
146
  def _rpc_error(mid, code: int, message: str) -> dict:
@@ -206,6 +207,10 @@ async def mcp_endpoint(request: Request):
206
207
  # MCP version. Otherwise a newer connector gets 400 instead of the 401
207
208
  # challenge that tells it how to sign in.
208
209
  user = _require_caller(request)
210
+ if request.headers.get('x-erp-connection-scope') == 'shared':
211
+ # A gateway can restrict its shared credential; this never grants
212
+ # permissions or changes who the credential belongs to.
213
+ user = {**user, '_shared_connection': True}
209
214
  try:
210
215
  body = await request.json()
211
216
  except Exception:
api/services.py CHANGED
@@ -358,9 +358,11 @@ def run_registered_action(name: str, args: dict | None, user_info: dict | None =
358
358
  raise PermissionError(
359
359
  f"Action '{name}' requires {meta['minimum_role']} access."
360
360
  )
361
- if meta.get("pass_user"):
362
- return meta["handler"](args or {}, user_info)
363
- return meta["handler"](args or {})
361
+ from api.write_context import write_actor
362
+ with write_actor(user_info):
363
+ if meta.get("pass_user"):
364
+ return meta["handler"](args or {}, user_info)
365
+ return meta["handler"](args or {})
364
366
 
365
367
 
366
368
  def registered_action_handlers(user_info: dict | None = None) -> dict:
api/write_context.py ADDED
@@ -0,0 +1,27 @@
1
+ """Authenticated write identity propagated to document lifecycle hooks.
2
+
3
+ Set only at server dispatch boundaries, never from document/model arguments.
4
+ ContextVar isolates concurrent requests and is copied by asyncio.to_thread.
5
+ """
6
+ from contextlib import contextmanager
7
+ from contextvars import ContextVar
8
+
9
+ _ACTOR = ContextVar("document_write_actor", default=None)
10
+
11
+
12
+ def current_write_actor():
13
+ return _ACTOR.get()
14
+
15
+
16
+ @contextmanager
17
+ def write_actor(user):
18
+ token = _ACTOR.set(user)
19
+ try:
20
+ yield
21
+ finally:
22
+ _ACTOR.reset(token)
23
+
24
+
25
+ def run_as_actor(user, handler, *args):
26
+ with write_actor(user):
27
+ return handler(*args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -2,7 +2,7 @@ api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  api/attachments.py,sha256=v5WAGUafYqA4tHWkPHHnP5pYxsJx9wDf6Mn1fTyUmYc,20496
3
3
  api/auth.py,sha256=xkyMjAFO1RpiwevPzjEw194x5M06F54LLLBx93RsV5Q,43203
4
4
  api/bootstrap.py,sha256=9r6Bz93Puhz06d8C5LsbYk2NUt2Gr6Nz85FJxOWriCc,21308
5
- api/chat.py,sha256=O_-kIF926Mt5AmamzlzO5RnDFxTFAp7oSjSApz1Cnyw,240993
5
+ api/chat.py,sha256=-FLrT5znrDJs1YrUB6gsmEbg9dQEI3bvQ2Em7qHKNTI,241088
6
6
  api/chat_links.py,sha256=eHuGxuMvWh42-UC3i97GVxV04NemXGMQDUhfoKiLO-A,2344
7
7
  api/demo_limits.py,sha256=b5PSNWgPIRuIVsAcJZyGLCivUv4RorEzJlZXf5LNwh0,14191
8
8
  api/deps.py,sha256=HaFNz0q0NlX49kNMs1xMe0rrO6Slqpt8-TlPOATirT4,140
@@ -11,7 +11,7 @@ api/errors.py,sha256=kEC6BUU4koPlyGoCDobAA8sHRoMqw50Xtc2105ZFKF4,5376
11
11
  api/list_values.py,sha256=mGe74gJ4fHPa5X2djJNTsRE8txa0hG40GAxMfTumU_w,963
12
12
  api/main.py,sha256=1rwZsXJRxIooFECRTZd-UwRUAGMp6SPL_sxLOCDploo,10803
13
13
  api/mcp_oauth.py,sha256=MjmScAvihkvEXdmM3QJP0FLqqXq-pQ1zmWa67LiArDM,21392
14
- api/mentions.py,sha256=lQa2E77cKQtRZMA7H5WMx78jI55JQJVkFKYJaCIO018,12910
14
+ api/mentions.py,sha256=3A4hJ8mUD9IVisz21x6XtsHeR0tHqMyIbLPRn3mb9Ps,16712
15
15
  api/oauth.py,sha256=nRJGGweUGvD_yN7KBSuO7dhL7ynSnCjiPCQdvaWDgnY,22493
16
16
  api/pdf.py,sha256=HnJzRYVx6D43ULvYCb2ehwqrGVvaqbzT1vsNg1Gxg7M,19228
17
17
  api/pdf_contract.py,sha256=cEUntXz951OaXTvGdhYnL58LbR_efowGGHhNEBP7y5Q,16118
@@ -19,9 +19,10 @@ api/pdf_exports.py,sha256=bFdfJVInkCH3BV8e9PpBLF9sOX2OkcFnNO0emuVnrps,3199
19
19
  api/pdf_profiles.py,sha256=gqMRM-T6nzQnJ0DTxUf9MBFtpJws07MpNUQHHgI3Y7c,9206
20
20
  api/providers.py,sha256=ZwuDtukTpHh553wln-AEvRKQ5kE4296rj5_Kgh84O30,6525
21
21
  api/remarks_md.py,sha256=8MZ5oO3TNLDbjdMF2PjdFW13LdAffogOxT2nT0eYrVY,3754
22
- api/services.py,sha256=-1SMUBPxTrRUAiOGIpQ3fZDZ4dDmWwEfTSrpslibOFE,61202
22
+ api/services.py,sha256=tXgkxV-U-KymK9xDo5Q6ILZg-roD9c8Uhmi6Uq4qPMI,61293
23
23
  api/time_filters.py,sha256=nAlyyZD7i3UOMq8kJdLGo2-jKYl84IZ8ad-oYBTjg1c,4882
24
24
  api/tool_permissions.py,sha256=CgVUUFLuJxEjQ44EWhzpIDNpzIY_tjSrgb5o5SBXU2Y,2216
25
+ api/write_context.py,sha256=RsrpMTa7k78_wWPGGE2KwLmIOdt_hF54kji5HdjGNI0,649
25
26
  api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
27
  api/routers/accounting.py,sha256=XRs5yGWMxSHN7cw2roslpYYrpK2rtzDgqwRdvMwe6VU,2529
27
28
  api/routers/actions.py,sha256=Pp3p88_coREd5KWkp_73IM_8ufVS_Kj6BRMRcEr5jY8,825
@@ -31,9 +32,9 @@ api/routers/availability.py,sha256=6iyVtIksPI3aC1tFFNAo4bwQ3iTiv3ebRmfhJzJrKPY,6
31
32
  api/routers/bank_reconciliation.py,sha256=USv3-WEsgmXvpXALkwVvE9E536hYQ8Wdlr3c1znyBiQ,3717
32
33
  api/routers/bank_statements.py,sha256=8d6ur-ZS7BCFv8mgdUJ7Il7qKjOxCj4rWdQgS1K4qIA,4518
33
34
  api/routers/chat_api.py,sha256=4XAtp3wszIkVB5n9IrPLscK3vNT4QHx5FRYk8JyJK3s,7084
34
- api/routers/documents.py,sha256=DG8fRnr5e-54xpmI1wp8i9CH1lJbSEHkg832HkMMnmk,11600
35
- api/routers/masters.py,sha256=rsXNEnBwwnLqUPSynH8DEv1HB2v-ZTh7nqxb_Ve9a8I,38907
36
- api/routers/mcp.py,sha256=x28NFgnNIXFPC7x_Im3eWRykafl1vjM4X93WGGufqRs,10625
35
+ api/routers/documents.py,sha256=Ww3bL-BN0uIYPRg6UO-Gq3YjtC-nX2DUYNKOqkZOn0Q,11736
36
+ api/routers/masters.py,sha256=hg8eO_TsN57ULrM20xTbf_P7sGQpAyRhGnIi9ZgoKRQ,39043
37
+ api/routers/mcp.py,sha256=6MP7TYw-Its0f7uNfsa4CbgFoQUqongB0vsxwPGj2iM,10948
37
38
  api/routers/proposals.py,sha256=KtIITJ-GHa3GSMzSDE205nnFNvMwt0KUVAxU1M8-uyU,4520
38
39
  api/routers/reports.py,sha256=6GLlUJQfstS2p_7F5Il_XYwmZIifSHqaVjFdxTQM1OU,29738
39
40
  api/routers/setup.py,sha256=4qVLO0TExVCTGORDI4dNMUyxauvhLRbUS1VydnSubNw,16206
@@ -103,7 +104,7 @@ lambda_erp/stock/delivery_note.py,sha256=Z3s57gVA7oSIpLauSd3DIWu-1y3tmvEE-zsopTv
103
104
  lambda_erp/stock/purchase_receipt.py,sha256=E42oHRO4MAowQniLumbOjYY38vVfHbHchkxCWyM3KDg,11463
104
105
  lambda_erp/stock/stock_entry.py,sha256=Itbt9a1u8b1s1OItKwVYMJDLjbNze2-UI2raIemCuts,12493
105
106
  lambda_erp/stock/stock_ledger.py,sha256=AFybuScWwfaTvsqOSAon3gwsjIza7skXvrvyQowL7b0,13265
106
- lambda_erp-1.2.0.dist-info/METADATA,sha256=2QjarnyxgDGIKrIvfWnIeTiElZDK8hcGrpJe2Um2FAw,31852
107
- lambda_erp-1.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
108
- lambda_erp-1.2.0.dist-info/licenses/LICENSE,sha256=8H08P22jiM0mJ97SgnpYchU0NeaPunGxZc3IqEaLgtc,11394
109
- lambda_erp-1.2.0.dist-info/RECORD,,
107
+ lambda_erp-1.2.2.dist-info/METADATA,sha256=P5e1cb8A_f9b5tY9YjS2mDJMV9Ph5Qft5LZCoyK35Aw,31852
108
+ lambda_erp-1.2.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
109
+ lambda_erp-1.2.2.dist-info/licenses/LICENSE,sha256=8H08P22jiM0mJ97SgnpYchU0NeaPunGxZc3IqEaLgtc,11394
110
+ lambda_erp-1.2.2.dist-info/RECORD,,