initial: split from gov-agreg — vreau.digital standalone platform
Moved from gov-agreg/src/pages/achizitii/* to root (drop prefix). - 22 pages migrated, 127 files total - All internal links: /achizitii/X → /X (176 occurrences fixed) - AchizitiiLayout subnav rewritten: /X paths, top-right link to vreaudigital.ro hub - BaseLayout new (vreau.digital branding, OG tags, site URL) - astro.config.mjs: site https://vreau.digital, server output (was static) - docker-compose: port 5096 (vreaudigital is 5095), container vreau-digital - deploy.sh: paths /opt/vreau-digital, log /var/log/vreau-digital-deploy.log Backend shared with gov-agreg: - PostgreSQL satra (same schemas: seap, firms, anaf, anre, ...) - Photon, Martin tiles - Infisical /vreaudigital path (DATABASE_URL etc. shared) build: PASS (npx astro check 0 errors, npm run build 5s vite + 10s server)
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
Base notice parser — shared logic for CN, PI, RFQ, DC, PC, Rdc, EN notices.
|
||||
|
||||
CA notice has more complex structure (lots + winners) so it has its own parser.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..xml_utils import (
|
||||
find_child_local, find_local, find_path,
|
||||
text_under, text_direct, int_under, decimal_under, bool_under,
|
||||
datetime_under, sysitem_name, sysitem_id,
|
||||
)
|
||||
|
||||
|
||||
def parse_basic_notice(el, *, type_tag: str, source_tag: str,
|
||||
notice_id_field: str = None) -> dict | None:
|
||||
"""Generic notice parser — works for CN, PI, DC, PC, Rdc, EN, RFQ.
|
||||
|
||||
type_tag: short type identifier for the row (e.g. 'c_notice', 'pi_notice')
|
||||
source_tag: value for source column (e.g. 'wsp_cnotice')
|
||||
notice_id_field: local name of the ID field (CNoticeId, PiNoticeId, etc.)
|
||||
If None, auto-detect by trying common names.
|
||||
"""
|
||||
notice_no = (text_under(el, 'NoticeNo') or
|
||||
text_under(el, 'CNoticeNumber') or
|
||||
text_under(el, 'PiNoticeNumber') or
|
||||
text_under(el, 'NoticeNumber') or
|
||||
text_under(el, 'RFQInvitationNumber') or
|
||||
text_under(el, 'RFQNoticeNumber') or
|
||||
text_under(el, 'DCNoticeNumber') or
|
||||
text_under(el, 'PCNoticeNumber') or
|
||||
text_under(el, 'RDCNoticeNumber') or
|
||||
text_under(el, 'ENoticeNumber') or
|
||||
text_under(el, 'EAProcedureNumber') or
|
||||
text_under(el, 'DfNoticeNo')) # last-resort fallback
|
||||
if not notice_no:
|
||||
return None
|
||||
|
||||
notice_id = None
|
||||
for field in (notice_id_field, 'CNoticeId', 'PiNoticeId', 'NoticeId',
|
||||
'CaNoticeId', 'DCNoticeId', 'PCNoticeId',
|
||||
'RFQNoticeId', 'RdcNoticeId', 'ENoticeId',
|
||||
'EAProcedureId', 'RFQInvitationId'):
|
||||
if field:
|
||||
notice_id = int_under(el, field)
|
||||
if notice_id is not None:
|
||||
break
|
||||
|
||||
general = find_child_local(el, 'General')
|
||||
section1 = find_child_local(el, 'Section1')
|
||||
section2 = find_child_local(el, 'Section2')
|
||||
section4 = find_child_local(el, 'Section4')
|
||||
|
||||
# Authority
|
||||
auth_addresses = find_path(section1, 'Section1_1', 'CaAddresses')
|
||||
if auth_addresses is None:
|
||||
auth_addresses = find_path(section1, 'Section1_1')
|
||||
auth_info = find_local(auth_addresses, 'EntityInformation') if auth_addresses is not None else None
|
||||
|
||||
authority_name = text_direct(auth_info, 'Name') if auth_info is not None else None
|
||||
authority_cui = text_direct(auth_info, 'Cif') if auth_info is not None else None
|
||||
authority_address = text_direct(auth_info, 'Address') if auth_info is not None else None
|
||||
authority_email = text_direct(auth_info, 'Email') if auth_info is not None else None
|
||||
authority_phone = text_direct(auth_info, 'Phone') if auth_info is not None else None
|
||||
authority_url = text_direct(auth_info, 'Url') if auth_info is not None else None
|
||||
county_code = sysitem_name(auth_info, 'NutsCode') if auth_info is not None else None
|
||||
entity_id = int_under(general, 'EntityId') if general is not None else None
|
||||
|
||||
s1_4 = find_child_local(section1, 'Section1_4') if section1 is not None else None
|
||||
authority_type = sysitem_name(s1_4, 'ContractingAuthorityType')
|
||||
s1_5 = find_child_local(section1, 'Section1_5') if section1 is not None else None
|
||||
main_activity = sysitem_name(s1_5, 'MainActivity')
|
||||
|
||||
# Section 2 — contract
|
||||
s2_1 = find_child_local(section2, 'Section2_1') if section2 is not None else None
|
||||
contract_title = (text_under(general, 'ContractTitle') or
|
||||
text_under(s2_1, 'ContractName') or
|
||||
text_under(s2_1, 'Title'))
|
||||
short_desc = text_under(s2_1, 'ShortContractDescription')
|
||||
main_cpv_code = sysitem_name(s2_1, 'MainCPV') or sysitem_name(s2_1, 'MainCPVCode')
|
||||
main_cpv_id = sysitem_id(s2_1, 'MainCPV') or sysitem_id(s2_1, 'MainCPVCode')
|
||||
contract_type = sysitem_name(s2_1, 'SysAcquisitionContractType')
|
||||
currency = sysitem_name(s2_1, 'Currency')
|
||||
estimated_value = decimal_under(s2_1, 'EstimatedValue') or decimal_under(s2_1, 'TotalValue')
|
||||
has_lots = bool_under(s2_1, 'ContractHasLots')
|
||||
reference_number = text_under(s2_1, 'ReferenceNumber')
|
||||
|
||||
# Lots — for CN/RFQ/etc., lots in Section2_2
|
||||
lots = _extract_lots_simple(section2)
|
||||
lots_count = len(lots) if lots else None
|
||||
|
||||
# Procedure
|
||||
s4_1 = find_child_local(section4, 'Section4_1') if section4 is not None else None
|
||||
procedure_type = sysitem_name(s4_1, 'SysProcedureType')
|
||||
framework_agreement = bool_under(s4_1, 'FrameworkAgreement')
|
||||
|
||||
# Section 4_2 — deadlines
|
||||
s4_2 = find_child_local(section4, 'Section4_2') if section4 is not None else None
|
||||
deadline_submission = (datetime_under(s4_2, 'TenderAvailabilityDeadline') or
|
||||
datetime_under(s4_2, 'ReceiptTimeLimit') or
|
||||
datetime_under(s4_2, 'ReceiptDeadline'))
|
||||
opening_date = datetime_under(s4_2, 'TenderOpeningDate')
|
||||
|
||||
# Dates + state
|
||||
publication_date = datetime_under(general, 'PublishDate')
|
||||
legislation = sysitem_name(general, 'SysLegislationType') or sysitem_name(general, 'LegislationType')
|
||||
notice_state = sysitem_name(general, 'SysNoticeState')
|
||||
notice_state_id = sysitem_id(general, 'SysNoticeState')
|
||||
is_utility = bool_under(general, 'IsUtility')
|
||||
notice_no_joue = text_under(general, 'NoticeNoJoue') or text_under(general, 'JOUEPublicationNumber')
|
||||
|
||||
# Documents
|
||||
documents = _extract_documents(general)
|
||||
|
||||
return {
|
||||
'type': type_tag,
|
||||
'ref_number': f'WSP-{notice_no}',
|
||||
'authority_name': authority_name,
|
||||
'authority_cui': authority_cui,
|
||||
'authority_address': authority_address,
|
||||
'authority_email': authority_email,
|
||||
'authority_phone': authority_phone,
|
||||
'authority_url': authority_url,
|
||||
'authority_type': authority_type,
|
||||
'authority_main_activity': main_activity,
|
||||
'authority_entity_id': entity_id,
|
||||
'title': contract_title[:1000] if contract_title else None,
|
||||
'cpv_code': main_cpv_code,
|
||||
'contract_type': contract_type,
|
||||
'publication_date': publication_date,
|
||||
'estimated_value': estimated_value,
|
||||
'awarded_value': None,
|
||||
'currency': currency,
|
||||
'supplier_name': None,
|
||||
'supplier_cui': None,
|
||||
'procedure_type': procedure_type,
|
||||
'procedure_state': notice_state,
|
||||
'legislation': legislation,
|
||||
'has_lots': 'da' if has_lots else 'nu' if has_lots is False else None,
|
||||
'contract_has_lots': has_lots,
|
||||
'lots_count': lots_count,
|
||||
'joue': notice_no_joue,
|
||||
'county_code': county_code,
|
||||
'notice_state': notice_state,
|
||||
'notice_state_id': notice_state_id,
|
||||
'framework_agreement': framework_agreement,
|
||||
'notice_id_internal': notice_id,
|
||||
'deadline_submission': deadline_submission,
|
||||
'opening_date': opening_date,
|
||||
'documents': documents or None,
|
||||
'lots': lots or None,
|
||||
'details': {
|
||||
'short_description': short_desc,
|
||||
'reference_number': reference_number,
|
||||
'main_cpv_id': main_cpv_id,
|
||||
'is_utility': is_utility,
|
||||
},
|
||||
'source': source_tag,
|
||||
}
|
||||
|
||||
|
||||
def _extract_lots_simple(section2) -> list[dict]:
|
||||
"""Extract Lots list from Section2_2 → Lots → LotInfo."""
|
||||
if section2 is None:
|
||||
return []
|
||||
lots_list = find_local(section2, 'Lots')
|
||||
if lots_list is None:
|
||||
return []
|
||||
out = []
|
||||
for lot in lots_list:
|
||||
if etree.QName(lot.tag).localname != 'LotInfo':
|
||||
continue
|
||||
lot_data = {
|
||||
'lot_id': int_under(lot, 'LotID'),
|
||||
'lot_no': int_under(lot, 'LotNo'),
|
||||
'title': text_under(lot, 'Title'),
|
||||
'description': text_under(lot, 'DescriptionOfProcurement'),
|
||||
'cpv_code': sysitem_name(lot, 'MainCPVCode'),
|
||||
'estimated_value': _str_decimal(decimal_under(lot, 'EstimatedValue')),
|
||||
'duration_months': int_under(lot, 'DurationInMonths'),
|
||||
'duration_days': int_under(lot, 'DurationInDays'),
|
||||
'currency': sysitem_name(lot, 'Currency'),
|
||||
'place_of_performance': text_under(lot, 'MainSiteOrPlaceOfPerformance'),
|
||||
'is_community_financed': bool_under(lot, 'IsCommunityFinanced'),
|
||||
}
|
||||
out.append({k: v for k, v in lot_data.items() if v is not None})
|
||||
return out
|
||||
|
||||
|
||||
def _extract_documents(general) -> list[dict]:
|
||||
if general is None:
|
||||
return []
|
||||
out = []
|
||||
for fld in ('NoticeFiles', 'CompanyFiles', 'DfNoticeFiles'):
|
||||
container = find_local(general, fld)
|
||||
if container is None:
|
||||
continue
|
||||
for kvp in container:
|
||||
key = find_local(kvp, 'key')
|
||||
if key is None:
|
||||
key = find_local(kvp, 'Key')
|
||||
val = find_local(kvp, 'value')
|
||||
if val is None:
|
||||
val = find_local(kvp, 'Value')
|
||||
if key is not None and val is not None:
|
||||
out.append({'type': fld, 'name': key.text, 'id': val.text})
|
||||
return out
|
||||
|
||||
|
||||
def _str_decimal(d):
|
||||
return str(d) if d is not None else None
|
||||
|
||||
|
||||
from lxml import etree # noqa: E402
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_CNotices — Anunțuri de participare."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='c_notice', source_tag='wsp_cnotice',
|
||||
notice_id_field='CNoticeId')
|
||||
@@ -0,0 +1,287 @@
|
||||
"""Parse SU_CaNotices items (Anunțuri de atribuire) → seap.announcements row dict."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..xml_utils import (
|
||||
find_child_local, find_local, find_path,
|
||||
text_under, text_direct, int_under, decimal_under, bool_under,
|
||||
datetime_under, sysitem_name, sysitem_id, to_jsonable,
|
||||
)
|
||||
|
||||
SOURCE = 'wsp_canotice'
|
||||
|
||||
|
||||
def parse(el) -> dict | None:
|
||||
"""Extract a CaNoticeBase element (V1 or V2) into our normalized dict."""
|
||||
notice_no = text_under(el, 'NoticeNo')
|
||||
notice_id = int_under(el, 'CaNoticeId')
|
||||
if not notice_no:
|
||||
return None
|
||||
|
||||
general = find_child_local(el, 'General')
|
||||
section1 = find_child_local(el, 'Section1')
|
||||
section2 = find_child_local(el, 'Section2')
|
||||
section4 = find_child_local(el, 'Section4')
|
||||
section5 = find_child_local(el, 'Section5')
|
||||
|
||||
# Authority block: Section1_1 → CaAddresses → CaEntityInformation → EntityInformation
|
||||
auth_entity = find_path(section1, 'Section1_1', 'CaAddresses')
|
||||
auth_info = None
|
||||
if auth_entity is not None:
|
||||
auth_info = find_local(auth_entity, 'EntityInformation')
|
||||
|
||||
authority_name = text_direct(auth_info, 'Name') if auth_info is not None else None
|
||||
authority_cui = text_direct(auth_info, 'Cif') if auth_info is not None else None
|
||||
authority_address = text_direct(auth_info, 'Address') if auth_info is not None else None
|
||||
authority_email = text_direct(auth_info, 'Email') if auth_info is not None else None
|
||||
authority_phone = text_direct(auth_info, 'Phone') if auth_info is not None else None
|
||||
authority_url = text_direct(auth_info, 'Url') if auth_info is not None else None
|
||||
|
||||
# NUTS code = county-ish (RO213 etc.)
|
||||
nuts_el = find_local(auth_info, 'NutsCode') if auth_info is not None else None
|
||||
county_code = sysitem_name(auth_info, 'NutsCode') if auth_info is not None else None
|
||||
|
||||
# Authority type + main activity
|
||||
s1_4 = find_child_local(section1, 'Section1_4') if section1 is not None else None
|
||||
authority_type = sysitem_name(s1_4, 'ContractingAuthorityType')
|
||||
s1_5 = find_child_local(section1, 'Section1_5') if section1 is not None else None
|
||||
main_activity = sysitem_name(s1_5, 'MainActivity')
|
||||
|
||||
# Section 2: contract details
|
||||
s2_1 = find_child_local(section2, 'Section2_1') if section2 is not None else None
|
||||
contract_title = text_under(general, 'ContractTitle') or text_under(s2_1, 'ContractName')
|
||||
short_desc = text_under(s2_1, 'ShortContractDescription')
|
||||
main_cpv_id = sysitem_id(s2_1, 'MainCPV')
|
||||
main_cpv_code = sysitem_name(s2_1, 'MainCPV')
|
||||
contract_type = sysitem_name(s2_1, 'SysAcquisitionContractType')
|
||||
currency = sysitem_name(s2_1, 'Currency')
|
||||
total_value = decimal_under(s2_1, 'TotalValue')
|
||||
highest_offer = decimal_under(s2_1, 'HighestOffer')
|
||||
lowest_offer = decimal_under(s2_1, 'LowestOffer')
|
||||
has_lots = bool_under(s2_1, 'ContractHasLots')
|
||||
reference_number = text_under(s2_1, 'ReferenceNumber')
|
||||
|
||||
# Lots — Section 5 (ContractLotList) for awarded notices, Section 2_2 otherwise
|
||||
lots = _extract_lots(section5) or _extract_lots(section2)
|
||||
lots_count = len(lots) if lots else None
|
||||
|
||||
# Award criteria — extracted from lot info or top-level
|
||||
award_criteria = _extract_award_criteria(el)
|
||||
|
||||
# Section 4: procedure details
|
||||
s4_1 = find_child_local(section4, 'Section4_1') if section4 is not None else None
|
||||
procedure_type = sysitem_name(s4_1, 'SysProcedureType')
|
||||
framework_agreement = bool_under(s4_1, 'FrameworkAgreement')
|
||||
|
||||
# Dates
|
||||
publication_date = datetime_under(general, 'PublishDate')
|
||||
legislation = sysitem_name(general, 'SysLegislationType')
|
||||
notice_state = sysitem_name(general, 'SysNoticeState')
|
||||
notice_state_id = sysitem_id(general, 'SysNoticeState')
|
||||
is_utility = bool_under(general, 'IsUtility')
|
||||
notice_no_joue = text_under(general, 'NoticeNoJoue')
|
||||
entity_id = int_under(general, 'EntityId')
|
||||
|
||||
# Winners (suppliers) — extracted from lot info
|
||||
winners = _extract_winners(section5)
|
||||
# Pick first winner as primary; full list in lots JSONB
|
||||
primary_winner = winners[0] if winners else {}
|
||||
|
||||
# Documents — DfNoticeFiles, CompanyFiles
|
||||
documents = _extract_documents(general)
|
||||
|
||||
# Final award value
|
||||
awarded_value = total_value
|
||||
if not awarded_value and lots:
|
||||
# sum lot values
|
||||
try:
|
||||
awarded_value = sum(
|
||||
(lot.get('total_value') or 0) for lot in lots if lot.get('total_value')
|
||||
) or None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
# Standard columns
|
||||
'type': 'ca_notice',
|
||||
'ref_number': f'WSP-{notice_no}',
|
||||
'authority_name': authority_name,
|
||||
'authority_cui': authority_cui,
|
||||
'authority_address': authority_address,
|
||||
'authority_email': authority_email,
|
||||
'authority_phone': authority_phone,
|
||||
'authority_url': authority_url,
|
||||
'authority_type': authority_type,
|
||||
'authority_main_activity': main_activity,
|
||||
'authority_entity_id': entity_id,
|
||||
'title': contract_title[:1000] if contract_title else None,
|
||||
'cpv_code': main_cpv_code,
|
||||
'contract_type': contract_type,
|
||||
'publication_date': publication_date,
|
||||
'estimated_value': None,
|
||||
'awarded_value': awarded_value,
|
||||
'currency': currency,
|
||||
'supplier_name': primary_winner.get('name'),
|
||||
'supplier_cui': primary_winner.get('cif'),
|
||||
'supplier_address': primary_winner.get('address'),
|
||||
'supplier_is_sme': primary_winner.get('is_sme'),
|
||||
'procedure_type': procedure_type,
|
||||
'procedure_state': notice_state,
|
||||
'legislation': legislation,
|
||||
'lot_number': None,
|
||||
'has_lots': 'da' if has_lots else 'nu' if has_lots is False else None,
|
||||
'contract_has_lots': has_lots,
|
||||
'lots_count': lots_count,
|
||||
'joue': notice_no_joue,
|
||||
'county_code': county_code,
|
||||
'notice_state': notice_state,
|
||||
'notice_state_id': notice_state_id,
|
||||
'framework_agreement': framework_agreement,
|
||||
'notice_id_internal': notice_id,
|
||||
'seap_url': f'https://e-licitatie.ro/pub/notices/contract-award-notice/{notice_id}' if notice_id else None,
|
||||
|
||||
# Rich JSONB
|
||||
'documents': documents or None,
|
||||
'award_criteria': award_criteria or None,
|
||||
'lots': lots or None,
|
||||
'details': {
|
||||
'short_description': short_desc,
|
||||
'reference_number': reference_number,
|
||||
'main_cpv_id': main_cpv_id,
|
||||
'highest_offer': str(highest_offer) if highest_offer else None,
|
||||
'lowest_offer': str(lowest_offer) if lowest_offer else None,
|
||||
'is_utility': is_utility,
|
||||
'all_winners': winners or None,
|
||||
},
|
||||
'source': SOURCE,
|
||||
}
|
||||
|
||||
|
||||
def _extract_lots(section) -> list[dict]:
|
||||
"""Extract lot info — handles ContractLotList (Section5) or Lots (Section2_2)."""
|
||||
if section is None:
|
||||
return []
|
||||
lot_list = find_local(section, 'ContractLotList')
|
||||
if lot_list is None:
|
||||
lot_list = find_local(section, 'Lots')
|
||||
if lot_list is None:
|
||||
return []
|
||||
out = []
|
||||
for lot in lot_list:
|
||||
if etree.QName(lot.tag).localname not in ('ContractLotInfo', 'LotInfo'):
|
||||
continue
|
||||
lot_data = {
|
||||
'lot_id': int_under(lot, 'LotID') or int_under(lot, 'ContractNo'),
|
||||
'lot_no': int_under(lot, 'LotNo') or int_under(lot, 'ContractNo'),
|
||||
'title': text_under(lot, 'Title') or text_under(lot, 'ContractObjectName'),
|
||||
'description': text_under(lot, 'DescriptionOfProcurement') or text_under(lot, 'ContractObjectDescription'),
|
||||
'cpv_code': sysitem_name(lot, 'MainCPVCode'),
|
||||
'estimated_value': _decimal_or_none(text_under(lot, 'EstimatedValue')),
|
||||
'total_value': _decimal_or_none(text_under(lot, 'TotalValue')),
|
||||
'duration_months': int_under(lot, 'DurationInMonths'),
|
||||
'duration_days': int_under(lot, 'DurationInDays'),
|
||||
'currency': sysitem_name(lot, 'Currency'),
|
||||
'place_of_performance': text_under(lot, 'MainSiteOrPlaceOfPerformance'),
|
||||
'is_community_financed': bool_under(lot, 'IsCommunityFinanced'),
|
||||
'has_options': bool_under(lot, 'HasOptions'),
|
||||
'awarded_to_group': bool_under(lot, 'AwardedToGroupOfEcoOp'),
|
||||
}
|
||||
# Convert decimals to str for JSON safety
|
||||
for k in ('estimated_value', 'total_value'):
|
||||
if lot_data[k] is not None:
|
||||
lot_data[k] = str(lot_data[k])
|
||||
# only keep non-empty
|
||||
out.append({k: v for k, v in lot_data.items() if v is not None})
|
||||
return out
|
||||
|
||||
|
||||
def _extract_winners(section5) -> list[dict]:
|
||||
"""Extract winner info from ContractLotList → ContractorAddressList."""
|
||||
if section5 is None:
|
||||
return []
|
||||
out = []
|
||||
seen_cifs = set()
|
||||
lot_list = find_local(section5, 'ContractLotList')
|
||||
if lot_list is None:
|
||||
return []
|
||||
for lot in lot_list:
|
||||
addr_list = find_local(lot, 'ContractorAddressList')
|
||||
if addr_list is None:
|
||||
continue
|
||||
for sect in addr_list:
|
||||
entity = find_local(sect, 'EntityInformation')
|
||||
if entity is None:
|
||||
continue
|
||||
cif = text_direct(entity, 'Cif')
|
||||
if cif in seen_cifs:
|
||||
continue
|
||||
if cif:
|
||||
seen_cifs.add(cif)
|
||||
out.append({
|
||||
'name': text_direct(entity, 'Name'),
|
||||
'cif': cif,
|
||||
'address': text_direct(entity, 'Address'),
|
||||
'email': text_direct(entity, 'Email'),
|
||||
'phone': text_direct(entity, 'Phone'),
|
||||
'url': text_direct(entity, 'Url'),
|
||||
'nuts_code': sysitem_name(entity, 'NutsCode'),
|
||||
'is_sme': bool_under(sect, 'IsSme'),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _extract_award_criteria(el) -> list[dict]:
|
||||
"""Extract award criteria from lot info (MyPriceAwardCriterias / MyQualityAwardCriterias)."""
|
||||
out = []
|
||||
for crit_list_name in ('MyPriceAwardCriterias', 'MyQualityAwardCriterias'):
|
||||
for crit_list in el.iter():
|
||||
if etree.QName(crit_list.tag).localname != crit_list_name:
|
||||
continue
|
||||
for crit in crit_list:
|
||||
name = text_under(crit, 'Name') or text_under(crit, 'Description')
|
||||
weight = decimal_under(crit, 'Weight') or decimal_under(crit, 'Pondere')
|
||||
if name or weight:
|
||||
item = {'type': 'price' if 'Price' in crit_list_name else 'quality',
|
||||
'name': name}
|
||||
if weight is not None:
|
||||
item['weight'] = str(weight)
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
def _extract_documents(general) -> list[dict]:
|
||||
"""Extract document file references from DfNoticeFiles + CompanyFiles."""
|
||||
if general is None:
|
||||
return []
|
||||
out = []
|
||||
for fld in ('DfNoticeFiles', 'CompanyFiles', 'NoticeFiles'):
|
||||
container = find_local(general, fld)
|
||||
if container is None:
|
||||
continue
|
||||
for kvp in container:
|
||||
key = find_local(kvp, 'key')
|
||||
if key is None:
|
||||
key = find_local(kvp, 'Key')
|
||||
val = find_local(kvp, 'value')
|
||||
if val is None:
|
||||
val = find_local(kvp, 'Value')
|
||||
if key is not None and val is not None:
|
||||
out.append({
|
||||
'type': fld,
|
||||
'name': key.text,
|
||||
'id': val.text,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _decimal_or_none(s):
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
from decimal import Decimal
|
||||
return Decimal(s)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# late import to avoid circular
|
||||
from lxml import etree # noqa: E402
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Catalog_ListItems — Beletage product catalog."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..xml_utils import text_under, decimal_under, datetime_under, sysitem_name, to_jsonable, int_under
|
||||
|
||||
|
||||
def parse(el) -> dict | None:
|
||||
item_code = text_under(el, 'ItemCode') or text_under(el, 'CatalogItemCode')
|
||||
if not item_code:
|
||||
return None
|
||||
return {
|
||||
'item_code': item_code,
|
||||
'item_name': text_under(el, 'Name') or text_under(el, 'ItemName'),
|
||||
'cpv_code': sysitem_name(el, 'CpvCode'),
|
||||
'unit_price': decimal_under(el, 'UnitPrice') or decimal_under(el, 'Price'),
|
||||
'currency': sysitem_name(el, 'Currency'),
|
||||
'last_updated': datetime_under(el, 'LastUpdate'),
|
||||
'details': to_jsonable(el),
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_DCNotices — Concurs de soluții."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='dc_notice', source_tag='wsp_dcnotice',
|
||||
notice_id_field='DCNoticeId')
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_ENotices — Erate (corections to other notices)."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='e_notice', source_tag='wsp_enotice',
|
||||
notice_id_field='ENoticeId')
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_EAProcedure — Licitații electronice."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='ea_procedure', source_tag='wsp_eaprocedure',
|
||||
notice_id_field='EAProcedureId')
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_PCNotices — Anunțuri concesionari."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='pc_notice', source_tag='wsp_pcnotice',
|
||||
notice_id_field='PCNoticeId')
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_PiNotices — Anunțuri de intenție."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='pi_notice', source_tag='wsp_pinotice',
|
||||
notice_id_field='PiNoticeId')
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_RdcNotices — Rezultate desemnare castigator concurs (rare)."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='rdc_notice', source_tag='wsp_rdcnotice',
|
||||
notice_id_field='RdcNoticeId')
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_RfqInvitations — Cereri de oferta (RFQ invitations)."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='rfq_invitation', source_tag='wsp_rfq_invitation',
|
||||
notice_id_field='RFQInvitationId')
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SU_RfqNotices — Anunțuri de atribuire la cererea de oferta."""
|
||||
from ._base import parse_basic_notice
|
||||
|
||||
def parse(el):
|
||||
return parse_basic_notice(el, type_tag='rfq_notice', source_tag='wsp_rfq_notice',
|
||||
notice_id_field='RFQNoticeId')
|
||||
@@ -0,0 +1,83 @@
|
||||
"""SuContracts — Beletage's own contracts (writes to seap.beletage_contracts)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..xml_utils import (
|
||||
find_child_local, find_local, text_under, text_direct,
|
||||
int_under, decimal_under, bool_under, datetime_under,
|
||||
sysitem_name, sysitem_id, to_jsonable,
|
||||
)
|
||||
|
||||
|
||||
def parse(el) -> dict | None:
|
||||
contract_id = int_under(el, 'ContractId')
|
||||
if contract_id is None:
|
||||
return None
|
||||
|
||||
# Top-level fields directly under ContractItem
|
||||
contract_no = text_under(el, 'ContractNo')
|
||||
contract_title = text_under(el, 'ContractTitle')
|
||||
contract_value = decimal_under(el, 'ContractValue')
|
||||
default_currency_value = decimal_under(el, 'DefaultCurrencyContractValue')
|
||||
awarding_date = datetime_under(el, 'ContractAwardingDate')
|
||||
contract_date = datetime_under(el, 'ContractDate')
|
||||
publication_date = datetime_under(el, 'PublicationDate')
|
||||
duration_months = int_under(el, 'MonthsContractDuration')
|
||||
is_current = bool_under(el, 'IsCurrentVersion')
|
||||
is_rejected = bool_under(el, 'IsRejected')
|
||||
version_no = int_under(el, 'VersionNo')
|
||||
version_date = datetime_under(el, 'VersionDate')
|
||||
justification = text_under(el, 'Justification')
|
||||
additional_info = text_under(el, 'AdditionalInformation')
|
||||
contract_phase = sysitem_name(el, 'SysContractPhase')
|
||||
contract_state = sysitem_name(el, 'SysContractState')
|
||||
contract_type = sysitem_name(el, 'SysContractType')
|
||||
currency = sysitem_name(el, 'ContractValueCurrency')
|
||||
|
||||
# CA Notice details: nested under <CANotice><General>...</General></CANotice>
|
||||
ca_notice = find_child_local(el, 'CANotice')
|
||||
ca_notice_id = None
|
||||
ca_notice_no = None
|
||||
authority_name = None
|
||||
authority_cui = None
|
||||
|
||||
if ca_notice is not None:
|
||||
general = find_child_local(ca_notice, 'General')
|
||||
if general is not None:
|
||||
ca_notice_id = int_under(general, 'CaNoticeId')
|
||||
ca_notice_no = text_under(general, 'NoticeNo')
|
||||
|
||||
section1 = find_child_local(ca_notice, 'Section1')
|
||||
if section1 is not None:
|
||||
auth_addresses = find_local(section1, 'CaAddresses')
|
||||
if auth_addresses is not None:
|
||||
auth_info = find_local(auth_addresses, 'EntityInformation')
|
||||
if auth_info is not None:
|
||||
authority_name = text_direct(auth_info, 'Name')
|
||||
authority_cui = text_direct(auth_info, 'Cif')
|
||||
|
||||
return {
|
||||
'contract_id': contract_id,
|
||||
'contract_no': contract_no,
|
||||
'contract_title': contract_title[:1000] if contract_title else None,
|
||||
'contract_type': contract_type,
|
||||
'contract_phase': contract_phase,
|
||||
'contract_state': contract_state,
|
||||
'awarding_date': awarding_date.date() if awarding_date else None,
|
||||
'contract_date': contract_date.date() if contract_date else None,
|
||||
'publication_date': publication_date,
|
||||
'duration_months': duration_months,
|
||||
'contract_value': contract_value,
|
||||
'default_currency_value': default_currency_value,
|
||||
'currency': currency,
|
||||
'ca_notice_id': ca_notice_id,
|
||||
'ca_notice_no': ca_notice_no,
|
||||
'authority_name': authority_name,
|
||||
'authority_cui': authority_cui,
|
||||
'is_current_version': is_current,
|
||||
'is_rejected': is_rejected,
|
||||
'version_no': version_no,
|
||||
'version_date': version_date,
|
||||
'justification': justification,
|
||||
'additional_information': additional_info,
|
||||
'details': to_jsonable(el),
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"""SuDirectAcquisitions — Beletage's own direct acquisitions (won)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..xml_utils import (
|
||||
find_child_local, find_local, text_under, text_direct,
|
||||
int_under, decimal_under, datetime_under, sysitem_name, to_jsonable,
|
||||
)
|
||||
|
||||
|
||||
def parse(el) -> dict | None:
|
||||
da_id = int_under(el, 'DirectAcquisitionId') or int_under(el, 'DirectAcquisitionID')
|
||||
if da_id is None:
|
||||
return None
|
||||
|
||||
cpv_code = sysitem_name(el, 'CpvCode')
|
||||
return {
|
||||
'da_id': da_id,
|
||||
'da_name': text_under(el, 'DirectAcquisitionName'),
|
||||
'unique_identification_code': text_under(el, 'UniqueIdentificationCode'),
|
||||
'cpv_code': cpv_code,
|
||||
'cpv_name': cpv_code, # name == code in this WSDL
|
||||
'contract_type': sysitem_name(el, 'SysAcquisitionContractType'),
|
||||
'publication_date': datetime_under(el, 'PublicationDate'),
|
||||
'finalization_date': datetime_under(el, 'FinalizationDate'),
|
||||
'estimated_value': decimal_under(el, 'EstimatedValue') or decimal_under(el, 'EstimatedValueRon'),
|
||||
'closing_value': decimal_under(el, 'ClosingValue'),
|
||||
'currency': 'RON',
|
||||
'da_state': sysitem_name(el, 'SysDirectAcquisitionState'),
|
||||
'authority_id': int_under(el, 'ContractingAuthorityID'),
|
||||
'authority_name': text_under(el, 'ContractingAuthority'),
|
||||
'details': to_jsonable(el),
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"""SuInvoices — Beletage's own invoices (writes to seap.beletage_invoices)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..xml_utils import (
|
||||
find_child_local, find_local, text_under, text_direct,
|
||||
int_under, decimal_under, datetime_under, sysitem_name, to_jsonable,
|
||||
)
|
||||
|
||||
|
||||
def parse(el) -> dict | None:
|
||||
invoice_id = int_under(el, 'InvoiceId')
|
||||
if invoice_id is None:
|
||||
return None
|
||||
|
||||
# Sums of payments
|
||||
paid_value = None
|
||||
paid_at = None
|
||||
payments = find_local(el, 'Payments')
|
||||
if payments is not None:
|
||||
for pay in payments:
|
||||
v = decimal_under(pay, 'Value')
|
||||
d = datetime_under(pay, 'Date')
|
||||
if v is not None:
|
||||
paid_value = (paid_value or 0) + v
|
||||
if d is not None and (paid_at is None or d > paid_at):
|
||||
paid_at = d
|
||||
|
||||
return {
|
||||
'invoice_id': invoice_id,
|
||||
'invoice_no': text_under(el, 'InvoiceNumber') or text_under(el, 'InvoiceNo'),
|
||||
'invoice_date': (datetime_under(el, 'InvoiceDate') or datetime_under(el, 'IssueDate')),
|
||||
'due_date': datetime_under(el, 'DueDate'),
|
||||
'contract_id': int_under(el, 'ContractId'),
|
||||
'contract_no': text_under(el, 'ContractNo'),
|
||||
'authority_name': text_under(el, 'BuyerName') or text_under(el, 'AuthorityName'),
|
||||
'authority_cui': text_under(el, 'BuyerCif') or text_under(el, 'AuthorityCif'),
|
||||
'total_value': decimal_under(el, 'TotalValue') or decimal_under(el, 'GrandTotal'),
|
||||
'total_value_no_vat': decimal_under(el, 'TotalValueNoVat') or decimal_under(el, 'NetTotal'),
|
||||
'vat_value': decimal_under(el, 'VatValue') or decimal_under(el, 'VAT'),
|
||||
'currency': sysitem_name(el, 'Currency') or text_under(el, 'CurrencyCode'),
|
||||
'state': sysitem_name(el, 'SysInvoiceState') or text_under(el, 'State'),
|
||||
'paid_value': paid_value,
|
||||
'paid_at': paid_at,
|
||||
'details': to_jsonable(el),
|
||||
}
|
||||
|
||||
|
||||
def _date_only(dt):
|
||||
return dt.date() if dt else None
|
||||
Reference in New Issue
Block a user