├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── ksa_compliance ├── __init__.py ├── background_jobs.py ├── compliance_checks.py ├── config │ └── __init__.py ├── design.md ├── generate_xml.py ├── hooks.py ├── invoice.py ├── jinja.py ├── ksa_compliance │ ├── __init__.py │ ├── custom │ │ ├── address.json │ │ ├── branch.json │ │ ├── customer.json │ │ ├── item_tax_template.json │ │ ├── mode_of_payment.json │ │ ├── pos_invoice_item.json │ │ ├── sales_invoice.json │ │ ├── sales_invoice_item.json │ │ └── tax_category.json │ ├── dashboard_chart │ │ ├── integration_status │ │ │ └── integration_status.json │ │ └── invoice_integration_statistics │ │ │ └── invoice_integration_statistics.json │ ├── doctype │ │ ├── __init__.py │ │ ├── additional_buyer_ids │ │ │ ├── __init__.py │ │ │ ├── additional_buyer_ids.json │ │ │ └── additional_buyer_ids.py │ │ ├── additional_seller_ids │ │ │ ├── __init__.py │ │ │ ├── additional_seller_ids.json │ │ │ └── additional_seller_ids.py │ │ ├── registration_type │ │ │ ├── __init__.py │ │ │ ├── registration_type.js │ │ │ ├── registration_type.json │ │ │ ├── registration_type.py │ │ │ └── test_registration_type.py │ │ ├── sales_invoice_additional_fields │ │ │ ├── __init__.py │ │ │ ├── sales_invoice_additional_fields.js │ │ │ ├── sales_invoice_additional_fields.json │ │ │ ├── sales_invoice_additional_fields.py │ │ │ └── test_sales_invoice_additional_fields.py │ │ ├── zatca_business_settings │ │ │ ├── __init__.py │ │ │ ├── test_zatca_business_settings.py │ │ │ ├── zatca_business_settings.js │ │ │ ├── zatca_business_settings.json │ │ │ └── zatca_business_settings.py │ │ ├── zatca_egs │ │ │ ├── __init__.py │ │ │ ├── test_zatca_egs.py │ │ │ ├── zatca_egs.js │ │ │ ├── zatca_egs.json │ │ │ └── zatca_egs.py │ │ ├── zatca_integration_log │ │ │ ├── __init__.py │ │ │ ├── test_zatca_integration_log.py │ │ │ ├── zatca_integration_log.js │ │ │ ├── zatca_integration_log.json │ │ │ └── zatca_integration_log.py │ │ ├── zatca_invoice_counting_settings │ │ │ ├── __init__.py │ │ │ ├── test_zatca_invoice_counting_settings.py │ │ │ ├── zatca_invoice_counting_settings.js │ │ │ ├── zatca_invoice_counting_settings.json │ │ │ └── zatca_invoice_counting_settings.py │ │ ├── zatca_phase_1_business_settings │ │ │ ├── __init__.py │ │ │ ├── test_zatca_phase_1_business_settings.py │ │ │ ├── zatca_phase_1_business_settings.js │ │ │ ├── zatca_phase_1_business_settings.json │ │ │ └── zatca_phase_1_business_settings.py │ │ ├── zatca_precomputed_invoice │ │ │ ├── __init__.py │ │ │ ├── test_zatca_precomputed_invoice.py │ │ │ ├── zatca_precomputed_invoice.js │ │ │ ├── zatca_precomputed_invoice.json │ │ │ └── zatca_precomputed_invoice.py │ │ └── zatca_return_against_reference │ │ │ ├── __init__.py │ │ │ ├── zatca_return_against_reference.json │ │ │ └── zatca_return_against_reference.py │ ├── ksa_compliance_dashboard │ │ └── overall_integration │ │ │ └── overall_integration.json │ ├── number_card │ │ ├── accepted_invoices │ │ │ └── accepted_invoices.json │ │ ├── accepted_with_warnings_invoices │ │ │ └── accepted_with_warnings_invoices.json │ │ ├── ready_for_batch_invoices │ │ │ └── ready_for_batch_invoices.json │ │ ├── rejected_invoices │ │ │ └── rejected_invoices.json │ │ └── resend_invoices │ │ │ └── resend_invoices.json │ ├── page │ │ ├── __init__.py │ │ └── e_invoicing_sync │ │ │ ├── __init__.py │ │ │ ├── e_invoicing_sync.js │ │ │ └── e_invoicing_sync.json │ ├── print_format │ │ ├── __init__.py │ │ ├── zatca_phase_1_print_format │ │ │ ├── __init__.py │ │ │ └── zatca_phase_1_print_format.json │ │ ├── zatca_phase_1_print_format___pos_invoice │ │ │ ├── __init__.py │ │ │ └── zatca_phase_1_print_format___pos_invoice.json │ │ ├── zatca_phase_2_print_format │ │ │ ├── __init__.py │ │ │ └── zatca_phase_2_print_format.json │ │ └── zatca_phase_2_print_format___pos_invoice │ │ │ ├── __init__.py │ │ │ └── zatca_phase_2_print_format___pos_invoice.json │ ├── report │ │ ├── __init__.py │ │ ├── zatca_integration_details │ │ │ ├── __init__.py │ │ │ ├── zatca_integration_details.js │ │ │ ├── zatca_integration_details.json │ │ │ └── zatca_integration_details.py │ │ └── zatca_integration_summary │ │ │ ├── __init__.py │ │ │ ├── zatca_integration_summary.js │ │ │ ├── zatca_integration_summary.json │ │ │ └── zatca_integration_summary.py │ └── workspace │ │ └── zatca │ │ └── zatca.json ├── modules.txt ├── output_models │ ├── e_invoice_output_model.py │ └── xsd │ │ ├── common │ │ ├── CCTS_CCT_SchemaModule-2.1.xsd │ │ ├── UBL-CommonAggregateComponents-2.1.xsd │ │ ├── UBL-CommonBasicComponents-2.1.xsd │ │ ├── UBL-CommonExtensionComponents-2.1.xsd │ │ ├── UBL-CommonSignatureComponents-2.1.xsd │ │ ├── UBL-CoreComponentParameters-2.1.xsd │ │ ├── UBL-ExtensionContentDataType-2.1.xsd │ │ ├── UBL-QualifiedDataTypes-2.1.xsd │ │ ├── UBL-SignatureAggregateComponents-2.1.xsd │ │ ├── UBL-SignatureBasicComponents-2.1.xsd │ │ ├── UBL-UnqualifiedDataTypes-2.1.xsd │ │ ├── UBL-XAdESv132-2.1.xsd │ │ ├── UBL-XAdESv141-2.1.xsd │ │ └── UBL-xmldsig-core-schema-2.1.xsd │ │ └── maindoc │ │ └── UBL-Invoice-2.1.xsd ├── patches.txt ├── patches │ ├── _2024_02_27_add_counting_docs_for_existing_settings.py │ ├── _2024_03_20_update_blank_integration_status_in_additional_field.py │ ├── _2024_03_21_update_last_attempt_in_additional_fields.py │ ├── _2024_03_21_uuid_indexes.py │ ├── _2024_06_05_set_cli_setup_to_manual.py │ ├── _2024_06_13_remove_custom_fields_from_sales_invoice.py │ ├── _2024_07_08_set_siaf_is_latest.py │ ├── _2024_08_19_update_old_fatoora_url_in_business_settings.py │ ├── _2024_09_04_delete_obsolete_print_formats.py │ ├── _2024_09_18_migrate_zatca_files_under_site.py │ └── zatca.py ├── public │ └── js │ │ ├── branch.js │ │ ├── customer.js │ │ └── sales_invoice.js ├── standard_doctypes │ ├── branch.py │ ├── sales_invoice.py │ ├── sales_invoice_item.py │ └── tax_category.py ├── templates │ ├── __init__.py │ ├── csr-config.properties │ ├── e_invoice.xml │ └── pages │ │ └── __init__.py ├── throw.py ├── translation.py ├── translations │ └── ar.csv ├── zatca_api.py ├── zatca_cli.py ├── zatca_cli_setup.py └── zatca_files.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | node_modules 7 | __pycache__ 8 | .idea/ 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.7.3 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | # Run the formatter. 9 | - id: ruff-format 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | KSA Compliance 2 | -------------- 3 | 4 | A free and open-source Frappe application for KSA Compliance (ZATCA Integration), offering support for both Phase 1 and Phase 2. 5 | 6 | ### Main Features 7 | 8 | 1. ZATCA Phase 1 - compliance 9 | 2. ZATCA Phase 2 - compliance 10 | 3. Simplified invoice 11 | 4. Standard Invoice 12 | 5. Wizard onboarding 13 | 6. Automatic ZATCA CLI setup 14 | 7. Tax exemption reasons 15 | 8. ZATCA dashboard 16 | 9. Embedded Invoice QR without impacting storage 17 | 10. Embedded Invoice XML without impacting storage 18 | 11. ZATCA phase 1 print format 19 | 12. ZATCA phase 2 print format 20 | 13. Resend process 21 | 14. Rejection process 22 | 15. ZATCA Integration Live and Batch modes 23 | 16. Multi-company support 24 | 17. Multi-device setup 25 | 18. Embedded compliance checks log 26 | 19. System XML validation 27 | 20. Support ZATCA Sandbox 28 | 29 | ### How to Install 30 | 31 | - **Frappe Cloud:**\ 32 | One-click installing available if you are hosting on Frappe Cloud 33 | - **Self Hosting:** 34 | 35 | ``` 36 | bench get-app --branch master https://github.com/lavaloon-eg/ksa_compliance.git 37 | ``` 38 | 39 | ``` 40 | bench setup requirements 41 | ``` 42 | 43 | ``` 44 | bench  --site [your.site.name] install-app ksa_compliance 45 | ``` 46 | 47 | ``` 48 | bench  --site [your.site.name] migrate 49 | ``` 50 | 51 | ``` 52 | bench restart 53 | ``` 54 | 55 | 56 | ### Support 57 | 58 | ### Frappe Cloud: 59 | 60 | - If you are hosting on FC premium support is available 61 | 62 | ### Self Hosting: 63 | 64 | - If you need premium support please email: Info@lavaloon.com 65 | 66 | ### Community Support: 67 | 68 | - Available in GitHub discussions 69 | 70 | ### New Features and Bug report: 71 | 72 | - Please Create Github Issue after checking the existing issues 73 | - Please include bench information (i.e. output of `bench version`) 74 | - For invoice rejections, please attach or paste the generated invoice XML (from `Sales Invoice Additional Fields`), any validation warnings/errors, and screenshots of the `Sales Invoice` document 75 | - For paid features, you can email us: 76 | 77 | ### **Contributing** 78 | 79 | Will using this the same guidelines from ERPNext 80 | 81 | 1. [**Issue Guidelines**](https://github.com/frappe/erpnext/wiki/Issue-Guidelines "https://github.com/frappe/erpnext/wiki/issue-guidelines") 82 | 2. [**Pull Request Requirements**](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines "https://github.com/frappe/erpnext/wiki/contribution-guidelines") 83 | 84 | ### License 85 | 86 | Copyright (c) 2024 LavaLoon, The KSA Compliance App code is licensed as AGPL 87 | -------------------------------------------------------------------------------- /ksa_compliance/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from frappe.utils.logger import get_logger 4 | 5 | logger = get_logger('zatca', max_size=1_000_000) 6 | logger.setLevel(logging.INFO) 7 | 8 | 9 | __version__ = '0.47.0' 10 | -------------------------------------------------------------------------------- /ksa_compliance/background_jobs.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional, cast 3 | 4 | import frappe 5 | from frappe.query_builder import DocType 6 | from pypika import Order 7 | from pypika.queries import QueryBuilder 8 | from result import is_ok 9 | 10 | from ksa_compliance import logger 11 | from ksa_compliance.ksa_compliance.doctype.sales_invoice_additional_fields.sales_invoice_additional_fields import ( 12 | SalesInvoiceAdditionalFields, 13 | ) 14 | 15 | 16 | @frappe.whitelist() 17 | def add_batch_to_background_queue(check_date=datetime.date.today()): 18 | try: 19 | logger.info('Start Enqueue E-Invoices') 20 | frappe.enqueue( 21 | 'ksa_compliance.background_jobs.sync_e_invoices', 22 | check_date=check_date, 23 | queue='long', 24 | timeout=3480, # 58 minutes, so that we can run it hourly 25 | job_name='Sync E-Invoices', 26 | deduplicate=True, 27 | job_id=f'Sending invoices {check_date}', 28 | ) 29 | except Exception as ex: 30 | logger.error('An error occurred queueing the job', exc_info=ex) 31 | 32 | 33 | def sync_e_invoices( 34 | check_date: Optional[datetime.datetime | datetime.date] = None, batch_size: int = 100, dry_run: bool = False 35 | ): 36 | prefix = '[Dry run] ' if dry_run else '' 37 | logger.info(f'{prefix}Syncing with ZATCA in batches of {batch_size}') 38 | if check_date: 39 | logger.info(f'{prefix}Limiting sync to >= date: {check_date}') 40 | 41 | # We can't use a numerical offset and increment it by the number of records because of the nature of the query. 42 | # We're querying for draft sales invoice additional fields then submitting them. Let's say we start with offset 0 43 | # and get 100 sales invoice additional fields. We submit the 100 and increase the offset to 100. Then we query 44 | # for a 100 **draft** sales invoice additional fields with offset 100, which skips a 100 draft additional sales 45 | # invoice fields because the 100 that we wanted to skip are now submitted, not draft. 46 | # 47 | # If we kept the offset at 0, the loop would never terminate in dry_run mode because we never update status. 48 | # 49 | # The solution is to use the creation date itself as an offset/filter. We sort by it ascending, so after every 50 | # batch we can query for fields whose creation > the last creation in the previous batch 51 | if isinstance(check_date, datetime.date): 52 | offset = cast(Optional[datetime.datetime], datetime.datetime.combine(check_date, datetime.time.min)) 53 | else: 54 | offset = cast(Optional[datetime.datetime], check_date) 55 | 56 | while True: 57 | query = build_query(offset, batch_size) 58 | additional_field_docs = query.run(as_dict=True) 59 | if not additional_field_docs: 60 | break 61 | 62 | logger.info(f'{prefix}Syncing {len(additional_field_docs)} after date/time {offset}') 63 | offset = additional_field_docs[-1].creation 64 | 65 | for doc in additional_field_docs: 66 | try: 67 | logger.info(f'{prefix}Submitting {doc.name}') 68 | if dry_run: 69 | continue 70 | 71 | adf_doc = cast( 72 | SalesInvoiceAdditionalFields, frappe.get_doc('Sales Invoice Additional Fields', doc.name) 73 | ) 74 | result = adf_doc.submit_to_zatca() 75 | message = result.ok_value if is_ok(result) else result.err_value 76 | logger.info(f'{prefix}{doc.name}: {message}') 77 | frappe.db.commit() 78 | except Exception: 79 | logger.error(f'{prefix}Error submitting {doc.name}', exc_info=True) 80 | frappe.db.rollback() 81 | 82 | logger.info(f'{prefix}Sync Done') 83 | 84 | 85 | def build_query(check_date: Optional[datetime.datetime], limit: int) -> QueryBuilder: 86 | batch_status = ['Ready For Batch', 'Resend', 'Corrected'] 87 | doctype = DocType('Sales Invoice Additional Fields') 88 | query = ( 89 | frappe.qb.from_(doctype) 90 | .select(doctype.name, doctype.creation) 91 | .where((doctype.integration_status.isin(batch_status)) & (doctype.docstatus == 0)) 92 | ) 93 | if check_date: 94 | query = query.where(doctype.creation > check_date) 95 | query = query.orderby(doctype.creation, order=Order.asc).limit(limit) 96 | return query 97 | -------------------------------------------------------------------------------- /ksa_compliance/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/config/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/generate_xml.py: -------------------------------------------------------------------------------- 1 | from frappe import get_jenv 2 | 3 | 4 | def generate_xml_file(data: dict): 5 | env = get_jenv() 6 | lstrip = env.lstrip_blocks 7 | trim = env.trim_blocks 8 | env.lstrip_blocks = True 9 | env.trim_blocks = True 10 | try: 11 | template = env.get_template('ksa_compliance/templates/e_invoice.xml') 12 | return template.render( 13 | { 14 | 'invoice': data.get('invoice'), 15 | 'seller_details': data.get('seller_details'), 16 | 'buyer_details': data.get('buyer_details'), 17 | 'business_settings': data.get('business_settings'), 18 | } 19 | ) 20 | finally: 21 | env.lstrip_blocks = lstrip 22 | env.trim_blocks = trim 23 | -------------------------------------------------------------------------------- /ksa_compliance/hooks.py: -------------------------------------------------------------------------------- 1 | app_name = 'ksa_compliance' 2 | app_title = 'KSA Compliance' 3 | app_publisher = 'LavaLoon' 4 | app_description = 'KSA Compliance app for E-invoice' 5 | app_email = 'info@lavaloon.com' 6 | app_license = 'Copyright (c) 2023 LavaLoon' 7 | # required_apps = [] 8 | 9 | # Includes in 10 | # ------------------ 11 | 12 | # include js, css files in header of desk.html 13 | # app_include_css = "/assets/ksa_compliance/css/ksa_compliance.css" 14 | # app_include_js = "/assets/ksa_compliance/js/ksa_compliance.js" 15 | 16 | # include js, css files in header of web template 17 | # web_include_css = "/assets/ksa_compliance/css/ksa_compliance.css" 18 | # web_include_js = "/assets/ksa_compliance/js/ksa_compliance.js" 19 | 20 | # include custom scss in every website theme (without file extension ".scss") 21 | # website_theme_scss = "ksa_compliance/public/scss/website" 22 | 23 | # include js, css files in header of web form 24 | # webform_include_js = {"doctype": "public/js/doctype.js"} 25 | # webform_include_css = {"doctype": "public/css/doctype.css"} 26 | 27 | # include js in page 28 | # page_js = {"page" : "public/js/file.js"} 29 | 30 | # include js in doctype views 31 | # doctype_js = {"doctype" : "public/js/doctype.js"} 32 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 33 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 34 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 35 | 36 | doctype_js = { 37 | 'Customer': 'public/js/customer.js', 38 | 'Branch': 'public/js/branch.js', 39 | 'Sales Invoice': 'public/js/sales_invoice.js', 40 | } 41 | 42 | # Svg Icons 43 | # ------------------ 44 | # include app icons in desk 45 | # app_include_icons = "ksa_compliance/public/icons.svg" 46 | 47 | # Home Pages 48 | # ---------- 49 | 50 | # application home page (will override Website Settings) 51 | # home_page = "login" 52 | 53 | # website user home page (by Role) 54 | # role_home_page = { 55 | # "Role": "home_page" 56 | # } 57 | 58 | # Generators 59 | # ---------- 60 | 61 | # automatically create page for each record of this doctype 62 | # website_generators = ["Web Page"] 63 | 64 | # Jinja 65 | # ---------- 66 | 67 | # add methods and filters to jinja environment 68 | jinja = { 69 | 'methods': [ 70 | 'ksa_compliance.jinja.get_zatca_phase_1_qr_for_invoice', 71 | 'frappe.utils.data.rounded', 72 | 'ksa_compliance.jinja.get_phase_2_print_format_details', 73 | ], 74 | } 75 | 76 | # Installation 77 | # ------------ 78 | 79 | # before_install = "ksa_compliance.install.before_install" 80 | # after_install = "ksa_compliance.install.after_install" 81 | 82 | # Uninstallation 83 | # ------------ 84 | 85 | # before_uninstall = "ksa_compliance.uninstall.before_uninstall" 86 | # after_uninstall = "ksa_compliance.uninstall.after_uninstall" 87 | 88 | # Integration Setup 89 | # ------------------ 90 | # To set up dependencies/integrations with other apps 91 | # Name of the app being installed is passed as an argument 92 | 93 | # before_app_install = "ksa_compliance.utils.before_app_install" 94 | # after_app_install = "ksa_compliance.utils.after_app_install" 95 | 96 | # Integration Cleanup 97 | # ------------------- 98 | # To clean up dependencies/integrations with other apps 99 | # Name of the app being uninstalled is passed as an argument 100 | 101 | # before_app_uninstall = "ksa_compliance.utils.before_app_uninstall" 102 | # after_app_uninstall = "ksa_compliance.utils.after_app_uninstall" 103 | 104 | # Desk Notifications 105 | # ------------------ 106 | # See frappe.core.notifications.get_notification_config 107 | 108 | # notification_config = "ksa_compliance.notifications.get_notification_config" 109 | 110 | # Permissions 111 | # ----------- 112 | # Permissions evaluated in scripted ways 113 | 114 | # permission_query_conditions = { 115 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 116 | # } 117 | # 118 | # has_permission = { 119 | # "Event": "frappe.desk.doctype.event.event.has_permission", 120 | # } 121 | 122 | # DocType Class 123 | # --------------- 124 | # Override standard doctype classes 125 | 126 | # override_doctype_class = { 127 | # "ToDo": "custom_app.overrides.CustomToDo" 128 | # } 129 | 130 | # Document Events 131 | # --------------- 132 | # Hook on document methods and events 133 | 134 | # doc_events = { 135 | # "*": { 136 | # "on_update": "method", 137 | # "on_cancel": "method", 138 | # "on_trash": "method" 139 | # } 140 | # } 141 | 142 | doc_events = { 143 | 'Sales Invoice': { 144 | 'on_submit': 'ksa_compliance.standard_doctypes.sales_invoice.create_sales_invoice_additional_fields_doctype', 145 | 'validate': 'ksa_compliance.standard_doctypes.sales_invoice.validate_sales_invoice', 146 | 'before_cancel': 'ksa_compliance.standard_doctypes.sales_invoice.prevent_cancellation_of_sales_invoice', 147 | }, 148 | 'POS Invoice': { 149 | 'on_submit': 'ksa_compliance.standard_doctypes.sales_invoice.create_sales_invoice_additional_fields_doctype', 150 | 'validate': 'ksa_compliance.standard_doctypes.sales_invoice.validate_sales_invoice', 151 | 'before_cancel': 'ksa_compliance.standard_doctypes.sales_invoice.prevent_cancellation_of_sales_invoice', 152 | }, 153 | 'Branch': { 154 | 'validate': 'ksa_compliance.standard_doctypes.branch.validate_branch', 155 | }, 156 | } 157 | 158 | # Scheduled Tasks 159 | # --------------- 160 | 161 | scheduler_events = {'hourly_long': ['ksa_compliance.background_jobs.sync_e_invoices']} 162 | # "all": [ 163 | # "ksa_compliance.tasks.all" 164 | # ], 165 | # "daily": [ 166 | # "ksa_compliance.tasks.daily" 167 | # ], 168 | # "hourly": [ 169 | # "ksa_compliance.tasks.hourly" 170 | # ], 171 | # "weekly": [ 172 | # "ksa_compliance.tasks.weekly" 173 | # ], 174 | # "monthly": [ 175 | # "ksa_compliance.tasks.monthly" 176 | # ], 177 | # } 178 | 179 | # Testing 180 | # ------- 181 | 182 | # before_tests = "ksa_compliance.install.before_tests" 183 | 184 | # Overriding Methods 185 | # ------------------------------ 186 | # 187 | # override_whitelisted_methods = { 188 | # "frappe.desk.doctype.event.event.get_events": "ksa_compliance.event.get_events" 189 | # } 190 | # 191 | # each overriding function accepts a `data` argument; 192 | # generated from the base implementation of the doctype dashboard, 193 | # along with any modifications made in other Frappe apps 194 | # override_doctype_dashboards = { 195 | # "Task": "ksa_compliance.task.get_dashboard_data" 196 | # } 197 | 198 | # exempt linked doctypes from being automatically cancelled 199 | # 200 | # auto_cancel_exempted_doctypes = ["Auto Repeat"] 201 | 202 | # Ignore links to specified DocTypes when deleting documents 203 | # ----------------------------------------------------------- 204 | 205 | # ignore_links_on_delete = ["Communication", "ToDo"] 206 | 207 | # Request Events 208 | # ---------------- 209 | # before_request = ["ksa_compliance.utils.before_request"] 210 | # after_request = ["ksa_compliance.utils.after_request"] 211 | 212 | # Job Events 213 | # ---------- 214 | # before_job = ["ksa_compliance.utils.before_job"] 215 | # after_job = ["ksa_compliance.utils.after_job"] 216 | 217 | # User Data Protection 218 | # -------------------- 219 | 220 | # user_data_fields = [ 221 | # { 222 | # "doctype": "{doctype_1}", 223 | # "filter_by": "{filter_by}", 224 | # "redact_fields": ["{field_1}", "{field_2}"], 225 | # "partial": 1, 226 | # }, 227 | # { 228 | # "doctype": "{doctype_2}", 229 | # "filter_by": "{filter_by}", 230 | # "partial": 1, 231 | # }, 232 | # { 233 | # "doctype": "{doctype_3}", 234 | # "strict": False, 235 | # }, 236 | # { 237 | # "doctype": "{doctype_4}" 238 | # } 239 | # ] 240 | 241 | # Authentication and authorization 242 | # -------------------------------- 243 | 244 | # auth_hooks = [ 245 | # "ksa_compliance.auth.validate" 246 | # ] 247 | 248 | # fixtures = [ 249 | # ] 250 | 251 | # Auto generate type annotations for doctypes 252 | # Reference: https://github.com/frappe/frappe/pull/21776 253 | export_python_type_annotations = True 254 | -------------------------------------------------------------------------------- /ksa_compliance/invoice.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Literal 3 | 4 | 5 | class InvoiceMode(Enum): 6 | Auto = 'Let the system decide (both)' 7 | Simplified = 'Simplified Tax Invoices' 8 | Standard = 'Standard Tax Invoices' 9 | 10 | @staticmethod 11 | def from_literal(mode: str) -> 'InvoiceMode': 12 | for value in InvoiceMode: 13 | if value.value == mode: 14 | return value 15 | raise ValueError(f'Unknown value: {mode}') 16 | 17 | 18 | InvoiceType = Literal['Standard', 'Simplified'] 19 | -------------------------------------------------------------------------------- /ksa_compliance/jinja.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import datetime 3 | from base64 import b64encode 4 | from io import BytesIO 5 | from typing import cast, Optional 6 | 7 | import pyqrcode 8 | 9 | import frappe 10 | from erpnext.accounts.doctype.pos_invoice.pos_invoice import POSInvoice 11 | from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice 12 | from erpnext.setup.doctype.branch.branch import Branch 13 | from frappe.utils.data import get_time, getdate 14 | from ksa_compliance.ksa_compliance.doctype.zatca_business_settings.zatca_business_settings import ZATCABusinessSettings 15 | 16 | 17 | def get_zatca_phase_1_qr_for_invoice(invoice_name: str) -> str: 18 | values = get_qr_inputs(invoice_name) 19 | if values is None: 20 | return values 21 | decoded_string = generate_decoded_string(values) 22 | return generate_qrcode(decoded_string) 23 | 24 | 25 | def get_qr_inputs(invoice_name: str) -> list: 26 | invoice_doc: Optional[SalesInvoice] = None 27 | if frappe.db.exists('POS Invoice', invoice_name): 28 | invoice_doc = cast(POSInvoice, frappe.get_doc('POS Invoice', invoice_name)) 29 | elif frappe.db.exists('Sales Invoice', invoice_name): 30 | invoice_doc = cast(SalesInvoice, frappe.get_doc('Sales Invoice', invoice_name)) 31 | else: 32 | return None 33 | seller_name = invoice_doc.company 34 | phase_1_name = frappe.get_value('ZATCA Phase 1 Business Settings', {'company': seller_name}) 35 | if not phase_1_name: 36 | return None 37 | phase_1_settings = frappe.get_doc('ZATCA Phase 1 Business Settings', phase_1_name) 38 | if phase_1_settings.status == 'Disabled': 39 | return None 40 | seller_vat_reg_no = phase_1_settings.vat_registration_number 41 | time = invoice_doc.posting_time 42 | timestamp = format_date(invoice_doc.posting_date, time) 43 | grand_total = invoice_doc.grand_total 44 | total_vat = invoice_doc.total_taxes_and_charges 45 | # returned values should be ordered based on ZATCA Qr Specifications 46 | return [seller_name, seller_vat_reg_no, timestamp, grand_total, total_vat] 47 | 48 | 49 | def generate_decoded_string(values: list) -> str: 50 | encoded_text = '' 51 | for tag, value in enumerate(values, 1): 52 | encoded_text += encode_input(value, [tag]) 53 | # Decode hex result string into base64 format 54 | return b64encode(bytes.fromhex(encoded_text)).decode() 55 | 56 | 57 | def encode_input(input: str, tag: int) -> str: 58 | """ 59 | 1- Convert bytes of tag into hex format. 60 | 2- Convert bytes of encoded length of input into hex format. 61 | 3- Convert encoded input itself into hex format. 62 | 4- Concat All values into one string. 63 | """ 64 | encoded_tag = bytes(tag).hex() 65 | if type(input) is str: 66 | encoded_length = bytes([len(input.encode('utf-8'))]).hex() 67 | encoded_value = input.encode('utf-8').hex() 68 | else: 69 | encoded_length = bytes([len(str(input).encode('utf-8'))]).hex() 70 | encoded_value = str(input).encode('utf-8').hex() 71 | return encoded_tag + encoded_length + encoded_value 72 | 73 | 74 | def format_date(date: str, time: str) -> str: 75 | """ 76 | Format date & time into UTC format something like : " 2021-12-13T10:39:15Z" 77 | """ 78 | posting_date = getdate(date) 79 | time = get_time(time) 80 | combined_datetime = datetime.datetime.combine(posting_date, time) 81 | combined_utc = combined_datetime.astimezone(datetime.timezone.utc) 82 | time_stamp = combined_utc.strftime('%Y-%m-%dT%H:%M:%SZ') 83 | return time_stamp 84 | 85 | 86 | def generate_qrcode(data: str) -> str: 87 | if not data: 88 | return None 89 | qr = pyqrcode.create(data) 90 | with BytesIO() as buffer: 91 | qr.png(buffer, scale=7) 92 | buffer.seek(0) 93 | img_str = base64.b64encode(buffer.getvalue()).decode('utf-8') 94 | return img_str 95 | 96 | 97 | def get_phase_2_print_format_details(sales_invoice: SalesInvoice | POSInvoice) -> dict | None: 98 | settings_id = frappe.db.exists( 99 | 'ZATCA Business Settings', {'company': sales_invoice.company, 'enable_zatca_integration': True} 100 | ) 101 | if not settings_id: 102 | return None 103 | 104 | branch_doc = None 105 | has_branch_address = False 106 | settings = cast(ZATCABusinessSettings, frappe.get_doc('ZATCA Business Settings', settings_id)) 107 | if settings.enable_branch_configuration: 108 | if sales_invoice.branch: 109 | branch_doc = cast(Branch, frappe.get_doc('Branch', sales_invoice.branch)) 110 | if branch_doc.custom_company_address: 111 | has_branch_address = True 112 | seller_other_id, seller_other_id_name = get_seller_other_id(sales_invoice, settings) 113 | buyer_other_id, buyer_other_id_name = get_buyer_other_id(sales_invoice.customer) 114 | siaf = frappe.get_last_doc('Sales Invoice Additional Fields', {'sales_invoice': sales_invoice.name}) 115 | return { 116 | 'settings': settings, 117 | 'address': { 118 | 'street': branch_doc.custom_street if has_branch_address else settings.street, 119 | 'district': branch_doc.custom_district if has_branch_address else settings.district, 120 | 'city': branch_doc.custom_city if has_branch_address else settings.city, 121 | 'postal_code': branch_doc.custom_postal_code if has_branch_address else settings.postal_code, 122 | }, 123 | 'seller_other_id': seller_other_id, 124 | 'seller_other_id_name': seller_other_id_name, 125 | 'buyer_other_id': buyer_other_id, 126 | 'buyer_other_id_name': buyer_other_id_name, 127 | 'siaf': siaf, 128 | } 129 | 130 | 131 | def get_seller_other_id(sales_invoice: SalesInvoice | POSInvoice, settings: ZATCABusinessSettings) -> tuple: 132 | seller_other_ids = ['CRN', 'MOM', 'MLS', '700', 'SAG', 'OTH'] 133 | seller_other_id, seller_other_id_name = None, None 134 | if settings.enable_branch_configuration: 135 | if sales_invoice.branch: 136 | seller_other_id = frappe.get_value( 137 | 'Additional Seller IDs', {'parent': sales_invoice.branch, 'type_code': 'CRN'}, 'value' 138 | ) 139 | if not seller_other_id: 140 | for other_id in seller_other_ids: 141 | seller_other_id = frappe.get_value( 142 | 'Additional Seller IDs', {'parent': settings.name, 'type_code': other_id}, 'value' 143 | ) 144 | seller_other_id = seller_other_id.strip() or None if isinstance(seller_other_id, str) else seller_other_id 145 | if seller_other_id and seller_other_id != 'CRN': 146 | seller_other_id_name = frappe.get_value( 147 | 'Additional Seller IDs', {'parent': settings.name, 'type_code': other_id}, 'type_name' 148 | ) 149 | break 150 | return seller_other_id, seller_other_id_name or 'Commercial Registration Number' 151 | 152 | 153 | def get_buyer_other_id(customer: str) -> tuple: 154 | buyer_other_ids = ['TIN', 'CRN', 'MOM', 'MLS', '700', 'SAG', 'NAT', 'GCC', 'IQA', 'PAS', 'OTH'] 155 | buyer_other_id, buyer_other_id_name = None, None 156 | for other_id in buyer_other_ids: 157 | buyer_other_id = frappe.get_value('Additional Buyer IDs', {'parent': customer, 'type_code': other_id}, 'value') 158 | buyer_other_id = buyer_other_id.strip() or None if isinstance(buyer_other_id, str) else buyer_other_id 159 | if buyer_other_id and buyer_other_id != 'CRN': 160 | buyer_other_id_name = frappe.get_value( 161 | 'Additional Buyer IDs', {'parent': customer, 'type_code': other_id}, 'type_name' 162 | ) 163 | break 164 | return buyer_other_id, buyer_other_id_name or 'Commercial Registration Number' 165 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/custom/address.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-01-11 16:36:43.046289", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Address", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "custom_area", 23 | "fieldtype": "Data", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 6, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "custom_building_number", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Area/District", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2024-01-11 16:36:43.046289", 42 | "modified_by": "Administrator", 43 | "module": null, 44 | "name": "Address-custom_area", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": null, 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 0, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "sort_options": 0, 60 | "translatable": 1, 61 | "unique": 0, 62 | "width": null 63 | }, 64 | { 65 | "_assign": null, 66 | "_comments": null, 67 | "_liked_by": null, 68 | "_user_tags": null, 69 | "allow_in_quick_entry": 0, 70 | "allow_on_submit": 0, 71 | "bold": 0, 72 | "collapsible": 0, 73 | "collapsible_depends_on": null, 74 | "columns": 0, 75 | "creation": "2024-01-11 16:38:33.303654", 76 | "default": null, 77 | "depends_on": null, 78 | "description": null, 79 | "docstatus": 0, 80 | "dt": "Address", 81 | "fetch_from": null, 82 | "fetch_if_empty": 0, 83 | "fieldname": "custom_building_number", 84 | "fieldtype": "Data", 85 | "hidden": 0, 86 | "hide_border": 0, 87 | "hide_days": 0, 88 | "hide_seconds": 0, 89 | "idx": 5, 90 | "ignore_user_permissions": 0, 91 | "ignore_xss_filter": 0, 92 | "in_global_search": 0, 93 | "in_list_view": 0, 94 | "in_preview": 0, 95 | "in_standard_filter": 0, 96 | "insert_after": "address_line2", 97 | "is_system_generated": 0, 98 | "is_virtual": 0, 99 | "label": "Building Number", 100 | "length": 0, 101 | "mandatory_depends_on": null, 102 | "modified": "2024-01-11 16:38:33.303654", 103 | "modified_by": "Administrator", 104 | "module": null, 105 | "name": "Address-custom_building_number", 106 | "no_copy": 0, 107 | "non_negative": 0, 108 | "options": null, 109 | "owner": "Administrator", 110 | "permlevel": 0, 111 | "precision": "", 112 | "print_hide": 0, 113 | "print_hide_if_no_value": 0, 114 | "print_width": null, 115 | "read_only": 0, 116 | "read_only_depends_on": null, 117 | "report_hide": 0, 118 | "reqd": 0, 119 | "search_index": 0, 120 | "sort_options": 0, 121 | "translatable": 1, 122 | "unique": 0, 123 | "width": null 124 | } 125 | ], 126 | "custom_perms": [], 127 | "doctype": "Address", 128 | "links": [], 129 | "property_setters": [], 130 | "sync_on_migrate": 1 131 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/custom/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-02-04 14:02:35.834885", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Customer", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "custom_section_break_nxdgf", 23 | "fieldtype": "Section Break", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 24, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "companies", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Customer IDs", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2024-02-04 14:02:35.834885", 42 | "modified_by": "Administrator", 43 | "module": null, 44 | "name": "Customer-custom_section_break_nxdgf", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": null, 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 0, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "sort_options": 0, 60 | "translatable": 0, 61 | "unique": 0, 62 | "width": null 63 | }, 64 | { 65 | "_assign": null, 66 | "_comments": null, 67 | "_liked_by": null, 68 | "_user_tags": null, 69 | "allow_in_quick_entry": 0, 70 | "allow_on_submit": 0, 71 | "bold": 0, 72 | "collapsible": 0, 73 | "collapsible_depends_on": null, 74 | "columns": 0, 75 | "creation": "2024-02-04 14:02:36.074730", 76 | "default": null, 77 | "depends_on": null, 78 | "description": null, 79 | "docstatus": 0, 80 | "dt": "Customer", 81 | "fetch_from": null, 82 | "fetch_if_empty": 0, 83 | "fieldname": "custom_vat_registration_number", 84 | "fieldtype": "Data", 85 | "hidden": 0, 86 | "hide_border": 0, 87 | "hide_days": 0, 88 | "hide_seconds": 0, 89 | "idx": 25, 90 | "ignore_user_permissions": 0, 91 | "ignore_xss_filter": 0, 92 | "in_global_search": 0, 93 | "in_list_view": 0, 94 | "in_preview": 0, 95 | "in_standard_filter": 0, 96 | "insert_after": "custom_section_break_nxdgf", 97 | "is_system_generated": 0, 98 | "is_virtual": 0, 99 | "label": "VAT Registration Number", 100 | "length": 0, 101 | "mandatory_depends_on": null, 102 | "modified": "2024-02-04 14:02:36.074730", 103 | "modified_by": "Administrator", 104 | "module": null, 105 | "name": "Customer-custom_vat_registration_number", 106 | "no_copy": 0, 107 | "non_negative": 0, 108 | "options": null, 109 | "owner": "Administrator", 110 | "permlevel": 0, 111 | "precision": "", 112 | "print_hide": 0, 113 | "print_hide_if_no_value": 0, 114 | "print_width": null, 115 | "read_only": 0, 116 | "read_only_depends_on": null, 117 | "report_hide": 0, 118 | "reqd": 0, 119 | "search_index": 0, 120 | "sort_options": 0, 121 | "translatable": 1, 122 | "unique": 0, 123 | "width": null 124 | }, 125 | { 126 | "_assign": null, 127 | "_comments": null, 128 | "_liked_by": null, 129 | "_user_tags": null, 130 | "allow_in_quick_entry": 0, 131 | "allow_on_submit": 0, 132 | "bold": 0, 133 | "collapsible": 0, 134 | "collapsible_depends_on": null, 135 | "columns": 0, 136 | "creation": "2024-02-04 14:02:36.355009", 137 | "default": null, 138 | "depends_on": null, 139 | "description": null, 140 | "docstatus": 0, 141 | "dt": "Customer", 142 | "fetch_from": null, 143 | "fetch_if_empty": 0, 144 | "fieldname": "custom_additional_ids", 145 | "fieldtype": "Table", 146 | "hidden": 0, 147 | "hide_border": 0, 148 | "hide_days": 0, 149 | "hide_seconds": 0, 150 | "idx": 26, 151 | "ignore_user_permissions": 0, 152 | "ignore_xss_filter": 0, 153 | "in_global_search": 0, 154 | "in_list_view": 0, 155 | "in_preview": 0, 156 | "in_standard_filter": 0, 157 | "insert_after": "custom_vat_registration_number", 158 | "is_system_generated": 0, 159 | "is_virtual": 0, 160 | "label": "Additional IDs", 161 | "length": 0, 162 | "mandatory_depends_on": null, 163 | "modified": "2024-02-04 14:02:36.355009", 164 | "modified_by": "Administrator", 165 | "module": null, 166 | "name": "Customer-custom_additional_ids", 167 | "no_copy": 0, 168 | "non_negative": 0, 169 | "options": "Additional Buyer IDs", 170 | "owner": "Administrator", 171 | "permlevel": 0, 172 | "precision": "", 173 | "print_hide": 0, 174 | "print_hide_if_no_value": 0, 175 | "print_width": null, 176 | "read_only": 0, 177 | "read_only_depends_on": null, 178 | "report_hide": 0, 179 | "reqd": 0, 180 | "search_index": 0, 181 | "sort_options": 0, 182 | "translatable": 0, 183 | "unique": 0, 184 | "width": null 185 | } 186 | ], 187 | "custom_perms": [], 188 | "doctype": "Customer", 189 | "links": [], 190 | "property_setters": [], 191 | "sync_on_migrate": 1 192 | } 193 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/custom/item_tax_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-08-20 14:40:07.850730", 15 | "default": null, 16 | "depends_on": null, 17 | "description": "The considered tax category on submission of the invoice to ZATCA.", 18 | "docstatus": 0, 19 | "dt": "Item Tax Template", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "custom_zatca_item_tax_category", 23 | "fieldtype": "Select", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 5, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "disabled", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "ZATCA Item Tax Category", 39 | "length": 0, 40 | "link_filters": null, 41 | "mandatory_depends_on": null, 42 | "modified": "2024-08-20 14:40:07.850730", 43 | "modified_by": "Administrator", 44 | "module": null, 45 | "name": "Item Tax Template-custom_zatca_item_tax_category", 46 | "no_copy": 0, 47 | "non_negative": 0, 48 | "options": "\nStandard rate\nServices outside scope of tax / Not subject to VAT || {manual entry}\nExempt from Tax || Financial services mentioned in Article 29 of the VAT Regulations\nExempt from Tax || Life insurance services mentioned in Article 29 of the VAT Regulations\nExempt from Tax || Real estate transactions mentioned in Article 30 of the VAT Regulations\nExempt from Tax || Qualified Supply of Goods in Duty Free area\nZero rated goods || Export of goods\nZero rated goods || Export of services\nZero rated goods || The international transport of Goods\nZero rated goods || International transport of passengers\nZero rated goods || Services directly connected and incidental to a Supply of international passenger transport\nZero rated goods || Supply of a qualifying means of transport\nZero rated goods || Any services relating to Goods or passenger transportation as defined in article twenty five of these Regulations\nZero rated goods || Medicines and medical equipment\nZero rated goods || Qualifying metals\nZero rated goods || Private education to citizen\nZero rated goods || Private healthcare to citizen\nZero rated goods || Supply of qualified military goods", 49 | "owner": "Administrator", 50 | "permlevel": 0, 51 | "precision": "", 52 | "print_hide": 0, 53 | "print_hide_if_no_value": 0, 54 | "print_width": null, 55 | "read_only": 0, 56 | "read_only_depends_on": null, 57 | "report_hide": 0, 58 | "reqd": 0, 59 | "search_index": 0, 60 | "show_dashboard": 0, 61 | "sort_options": 0, 62 | "translatable": 1, 63 | "unique": 0, 64 | "width": null 65 | }, 66 | { 67 | "_assign": null, 68 | "_comments": null, 69 | "_liked_by": null, 70 | "_user_tags": null, 71 | "allow_in_quick_entry": 0, 72 | "allow_on_submit": 0, 73 | "bold": 0, 74 | "collapsible": 0, 75 | "collapsible_depends_on": null, 76 | "columns": 0, 77 | "creation": "2024-08-20 13:32:28.159840", 78 | "default": null, 79 | "depends_on": "eval:doc.custom_zatca_item_tax_category==\"Services outside scope of tax / Not subject to VAT || {manual entry}\"", 80 | "description": "The reason the service or item is out of scope of tax or is entered manually.", 81 | "docstatus": 0, 82 | "dt": "Item Tax Template", 83 | "fetch_from": null, 84 | "fetch_if_empty": 0, 85 | "fieldname": "custom_category_reason", 86 | "fieldtype": "Data", 87 | "hidden": 0, 88 | "hide_border": 0, 89 | "hide_days": 0, 90 | "hide_seconds": 0, 91 | "idx": 6, 92 | "ignore_user_permissions": 0, 93 | "ignore_xss_filter": 0, 94 | "in_global_search": 0, 95 | "in_list_view": 0, 96 | "in_preview": 0, 97 | "in_standard_filter": 0, 98 | "insert_after": "custom_zatca_item_tax_category", 99 | "is_system_generated": 0, 100 | "is_virtual": 0, 101 | "label": "Category Reason", 102 | "length": 0, 103 | "link_filters": null, 104 | "mandatory_depends_on": "eval:doc.custom_zatca_item_category==\"Services outside scope of tax / Not subject to VAT || {manual entry}\"", 105 | "modified": "2024-08-20 13:32:28.159840", 106 | "modified_by": "Administrator", 107 | "module": null, 108 | "name": "Item Tax Template-custom_category_reason", 109 | "no_copy": 0, 110 | "non_negative": 0, 111 | "options": null, 112 | "owner": "Administrator", 113 | "permlevel": 0, 114 | "precision": "", 115 | "print_hide": 0, 116 | "print_hide_if_no_value": 0, 117 | "print_width": null, 118 | "read_only": 0, 119 | "read_only_depends_on": null, 120 | "report_hide": 0, 121 | "reqd": 0, 122 | "search_index": 0, 123 | "show_dashboard": 0, 124 | "sort_options": 0, 125 | "translatable": 1, 126 | "unique": 0, 127 | "width": null 128 | } 129 | ], 130 | "custom_perms": [], 131 | "doctype": "Item Tax Template", 132 | "links": [], 133 | "property_setters": [ 134 | { 135 | "_assign": null, 136 | "_comments": null, 137 | "_liked_by": null, 138 | "_user_tags": null, 139 | "creation": "2024-08-20 14:43:57.482952", 140 | "default_value": null, 141 | "doc_type": "Item Tax Template", 142 | "docstatus": 0, 143 | "doctype_or_field": "DocType", 144 | "field_name": null, 145 | "idx": 0, 146 | "is_system_generated": 0, 147 | "modified": "2024-08-20 14:43:57.482952", 148 | "modified_by": "Administrator", 149 | "module": null, 150 | "name": "Item Tax Template-main-field_order", 151 | "owner": "Administrator", 152 | "property": "field_order", 153 | "property_type": "Data", 154 | "row_name": null, 155 | "value": "[\"title\", \"company\", \"column_break_3\", \"disabled\", \"custom_zatca_item_tax_category\", \"custom_category_reason\", \"section_break_5\", \"taxes\"]" 156 | } 157 | ], 158 | "sync_on_migrate": 1 159 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/custom/mode_of_payment.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-03-01 15:48:01.548358", 15 | "default": null, 16 | "depends_on": null, 17 | "description": "Value from UN/EDIFACT 4461", 18 | "docstatus": 0, 19 | "dt": "Mode of Payment", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "custom_zatca_payment_means_code", 23 | "fieldtype": "Data", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 5, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "accounts", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "ZATCA Payment Means Code", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2024-03-01 15:48:01.548358", 42 | "modified_by": "Administrator", 43 | "module": null, 44 | "name": "Mode of Payment-custom_zatca_payment_means_code", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": null, 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 0, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 1, 58 | "search_index": 0, 59 | "sort_options": 0, 60 | "translatable": 0, 61 | "unique": 0, 62 | "width": null 63 | } 64 | ], 65 | "custom_perms": [], 66 | "doctype": "Mode of Payment", 67 | "links": [], 68 | "property_setters": [ 69 | { 70 | "_assign": null, 71 | "_comments": null, 72 | "_liked_by": null, 73 | "_user_tags": null, 74 | "creation": "2024-03-01 15:55:13.629443", 75 | "default_value": null, 76 | "doc_type": "Mode of Payment", 77 | "docstatus": 0, 78 | "doctype_or_field": "DocType", 79 | "field_name": null, 80 | "idx": 0, 81 | "is_system_generated": 0, 82 | "modified": "2024-03-01 15:55:13.629443", 83 | "modified_by": "Administrator", 84 | "module": null, 85 | "name": "Mode of Payment-main-field_order", 86 | "owner": "Administrator", 87 | "property": "field_order", 88 | "property_type": "Data", 89 | "row_name": null, 90 | "value": "[\"mode_of_payment\", \"enabled\", \"type\", \"accounts\", \"custom_zatca_payment_means_code\"]" 91 | } 92 | ], 93 | "sync_on_migrate": 1 94 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/custom/pos_invoice_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-10-02 09:35:18.976152", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "POS Invoice Item", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "tax_amount", 23 | "fieldtype": "Currency", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 0, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "tax_rate", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Tax Amount", 39 | "length": 0, 40 | "link_filters": null, 41 | "mandatory_depends_on": null, 42 | "modified": "2024-10-02 10:27:30.290059", 43 | "modified_by": "Administrator", 44 | "module": null, 45 | "name": "POS Invoice Item-tax_amount", 46 | "no_copy": 0, 47 | "non_negative": 0, 48 | "options": "currency", 49 | "owner": "Administrator", 50 | "permlevel": 0, 51 | "precision": "", 52 | "print_hide": 1, 53 | "print_hide_if_no_value": 0, 54 | "print_width": null, 55 | "read_only": 1, 56 | "read_only_depends_on": null, 57 | "report_hide": 0, 58 | "reqd": 0, 59 | "search_index": 0, 60 | "show_dashboard": 0, 61 | "sort_options": 0, 62 | "translatable": 0, 63 | "unique": 0, 64 | "width": null 65 | }, 66 | { 67 | "_assign": null, 68 | "_comments": null, 69 | "_liked_by": null, 70 | "_user_tags": null, 71 | "allow_in_quick_entry": 0, 72 | "allow_on_submit": 0, 73 | "bold": 0, 74 | "collapsible": 0, 75 | "collapsible_depends_on": null, 76 | "columns": 0, 77 | "creation": "2024-10-02 09:35:18.971454", 78 | "default": null, 79 | "depends_on": null, 80 | "description": null, 81 | "docstatus": 0, 82 | "dt": "POS Invoice Item", 83 | "fetch_from": null, 84 | "fetch_if_empty": 0, 85 | "fieldname": "tax_rate", 86 | "fieldtype": "Float", 87 | "hidden": 0, 88 | "hide_border": 0, 89 | "hide_days": 0, 90 | "hide_seconds": 0, 91 | "idx": 0, 92 | "ignore_user_permissions": 0, 93 | "ignore_xss_filter": 0, 94 | "in_global_search": 0, 95 | "in_list_view": 0, 96 | "in_preview": 0, 97 | "in_standard_filter": 0, 98 | "insert_after": "custom_zatca_item_tax_category", 99 | "is_system_generated": 0, 100 | "is_virtual": 0, 101 | "label": "Tax Rate", 102 | "length": 0, 103 | "link_filters": null, 104 | "mandatory_depends_on": null, 105 | "modified": "2024-10-02 10:27:30.278300", 106 | "modified_by": "Administrator", 107 | "module": null, 108 | "name": "POS Invoice Item-tax_rate", 109 | "no_copy": 0, 110 | "non_negative": 0, 111 | "options": null, 112 | "owner": "Administrator", 113 | "permlevel": 0, 114 | "precision": "", 115 | "print_hide": 1, 116 | "print_hide_if_no_value": 0, 117 | "print_width": null, 118 | "read_only": 1, 119 | "read_only_depends_on": null, 120 | "report_hide": 0, 121 | "reqd": 0, 122 | "search_index": 0, 123 | "show_dashboard": 0, 124 | "sort_options": 0, 125 | "translatable": 0, 126 | "unique": 0, 127 | "width": null 128 | }, 129 | { 130 | "_assign": null, 131 | "_comments": null, 132 | "_liked_by": null, 133 | "_user_tags": null, 134 | "allow_in_quick_entry": 0, 135 | "allow_on_submit": 0, 136 | "bold": 0, 137 | "collapsible": 0, 138 | "collapsible_depends_on": null, 139 | "columns": 0, 140 | "creation": "2024-10-02 09:35:18.980721", 141 | "default": null, 142 | "depends_on": null, 143 | "description": null, 144 | "docstatus": 0, 145 | "dt": "POS Invoice Item", 146 | "fetch_from": null, 147 | "fetch_if_empty": 0, 148 | "fieldname": "total_amount", 149 | "fieldtype": "Currency", 150 | "hidden": 0, 151 | "hide_border": 0, 152 | "hide_days": 0, 153 | "hide_seconds": 0, 154 | "idx": 0, 155 | "ignore_user_permissions": 0, 156 | "ignore_xss_filter": 0, 157 | "in_global_search": 0, 158 | "in_list_view": 0, 159 | "in_preview": 0, 160 | "in_standard_filter": 0, 161 | "insert_after": "tax_amount", 162 | "is_system_generated": 0, 163 | "is_virtual": 0, 164 | "label": "Total Amount", 165 | "length": 0, 166 | "link_filters": null, 167 | "mandatory_depends_on": null, 168 | "modified": "2024-10-02 10:27:30.298473", 169 | "modified_by": "Administrator", 170 | "module": null, 171 | "name": "POS Invoice Item-total_amount", 172 | "no_copy": 0, 173 | "non_negative": 0, 174 | "options": "currency", 175 | "owner": "Administrator", 176 | "permlevel": 0, 177 | "precision": "", 178 | "print_hide": 1, 179 | "print_hide_if_no_value": 0, 180 | "print_width": null, 181 | "read_only": 1, 182 | "read_only_depends_on": null, 183 | "report_hide": 0, 184 | "reqd": 0, 185 | "search_index": 0, 186 | "show_dashboard": 0, 187 | "sort_options": 0, 188 | "translatable": 0, 189 | "unique": 0, 190 | "width": null 191 | } 192 | ], 193 | "custom_perms": [], 194 | "doctype": "POS Invoice Item", 195 | "links": [], 196 | "property_setters": [], 197 | "sync_on_migrate": 1 198 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/custom/sales_invoice.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2025-02-26 13:04:26.784771", 15 | "default": null, 16 | "depends_on": "eval:doc.is_return || doc.is_debit_note", 17 | "description": "Additional billing references to be sent to ZATCA", 18 | "docstatus": 0, 19 | "dt": "Sales Invoice", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "custom_return_against_additional_references", 23 | "fieldtype": "Table MultiSelect", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 21, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "return_against", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Return Against Additional References", 39 | "length": 0, 40 | "link_filters": null, 41 | "mandatory_depends_on": null, 42 | "modified": "2025-02-26 13:45:26.784771", 43 | "modified_by": "Administrator", 44 | "module": null, 45 | "name": "Sales Invoice-custom_return_against_additional_references", 46 | "no_copy": 1, 47 | "non_negative": 0, 48 | "options": "ZATCA Return Against Reference", 49 | "owner": "Administrator", 50 | "permlevel": 0, 51 | "placeholder": null, 52 | "precision": "", 53 | "print_hide": 0, 54 | "print_hide_if_no_value": 0, 55 | "print_width": null, 56 | "read_only": 0, 57 | "read_only_depends_on": null, 58 | "report_hide": 0, 59 | "reqd": 0, 60 | "search_index": 0, 61 | "show_dashboard": 0, 62 | "sort_options": 0, 63 | "translatable": 0, 64 | "unique": 0, 65 | "width": null 66 | }, 67 | { 68 | "_assign": null, 69 | "_comments": null, 70 | "_liked_by": null, 71 | "_user_tags": null, 72 | "allow_in_quick_entry": 0, 73 | "allow_on_submit": 0, 74 | "bold": 0, 75 | "collapsible": 0, 76 | "collapsible_depends_on": null, 77 | "columns": 0, 78 | "creation": "2024-01-24 17:38:26.701604", 79 | "default": null, 80 | "depends_on": "eval:doc.is_return || doc.is_debit_note", 81 | "description": null, 82 | "docstatus": 0, 83 | "dt": "Sales Invoice", 84 | "fetch_from": null, 85 | "fetch_if_empty": 0, 86 | "fieldname": "custom_return_reason", 87 | "fieldtype": "Data", 88 | "hidden": 0, 89 | "hide_border": 0, 90 | "hide_days": 0, 91 | "hide_seconds": 0, 92 | "idx": 27, 93 | "ignore_user_permissions": 0, 94 | "ignore_xss_filter": 0, 95 | "in_global_search": 0, 96 | "in_list_view": 0, 97 | "in_preview": 0, 98 | "in_standard_filter": 0, 99 | "insert_after": "is_debit_note", 100 | "is_system_generated": 0, 101 | "is_virtual": 0, 102 | "label": "Return/Debit Reason", 103 | "length": 0, 104 | "mandatory_depends_on": "eval:doc.is_return || doc.is_debit_note", 105 | "modified": "2024-01-24 17:38:26.701604", 106 | "modified_by": "Administrator", 107 | "module": null, 108 | "name": "Sales Invoice-custom_return_reason", 109 | "no_copy": 0, 110 | "non_negative": 0, 111 | "options": null, 112 | "owner": "Administrator", 113 | "permlevel": 0, 114 | "precision": "", 115 | "print_hide": 0, 116 | "print_hide_if_no_value": 0, 117 | "print_width": null, 118 | "read_only": 0, 119 | "read_only_depends_on": "", 120 | "report_hide": 0, 121 | "reqd": 0, 122 | "search_index": 0, 123 | "sort_options": 0, 124 | "translatable": 0, 125 | "unique": 0, 126 | "width": null 127 | }, 128 | { 129 | "_assign": null, 130 | "_comments": null, 131 | "_liked_by": null, 132 | "_user_tags": null, 133 | "allow_in_quick_entry": 0, 134 | "allow_on_submit": 0, 135 | "bold": 0, 136 | "collapsible": 0, 137 | "collapsible_depends_on": null, 138 | "columns": 0, 139 | "creation": "2024-02-08 11:10:47.663970", 140 | "default": null, 141 | "depends_on": null, 142 | "description": null, 143 | "docstatus": 0, 144 | "dt": "Sales Invoice", 145 | "fetch_from": null, 146 | "fetch_if_empty": 0, 147 | "fieldname": "custom_section_break_becvo", 148 | "fieldtype": "Section Break", 149 | "hidden": 0, 150 | "hide_border": 0, 151 | "hide_days": 0, 152 | "hide_seconds": 0, 153 | "idx": 103, 154 | "ignore_user_permissions": 0, 155 | "ignore_xss_filter": 0, 156 | "in_global_search": 0, 157 | "in_list_view": 0, 158 | "in_preview": 0, 159 | "in_standard_filter": 0, 160 | "insert_after": "total_billing_amount", 161 | "is_system_generated": 0, 162 | "is_virtual": 0, 163 | "label": "", 164 | "length": 0, 165 | "mandatory_depends_on": null, 166 | "modified": "2024-02-08 11:10:47.663970", 167 | "modified_by": "Administrator", 168 | "module": null, 169 | "name": "Sales Invoice-custom_section_break_becvo", 170 | "no_copy": 0, 171 | "non_negative": 0, 172 | "options": null, 173 | "owner": "Administrator", 174 | "permlevel": 0, 175 | "precision": "", 176 | "print_hide": 0, 177 | "print_hide_if_no_value": 0, 178 | "print_width": null, 179 | "read_only": 0, 180 | "read_only_depends_on": null, 181 | "report_hide": 0, 182 | "reqd": 0, 183 | "search_index": 0, 184 | "sort_options": 0, 185 | "translatable": 0, 186 | "unique": 0, 187 | "width": null 188 | } 189 | ], 190 | "custom_perms": [], 191 | "doctype": "Sales Invoice", 192 | "links": [], 193 | "property_setters": [], 194 | "sync_on_migrate": 1 195 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/custom/tax_category.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-06-04 18:12:33.251245", 15 | "default": null, 16 | "depends_on": "eval:doc.custom_zatca_category==\"Services outside scope of tax / Not subject to VAT || {manual entry}\"", 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Tax Category", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "custom_category_reason", 23 | "fieldtype": "Data", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 5, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "custom_zatca_category", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Category Reason", 39 | "length": 0, 40 | "link_filters": null, 41 | "mandatory_depends_on": "eval:doc.custom_zatca_category==\"Services outside scope of tax / Not subject to VAT || {manual entry}\"", 42 | "modified": "2024-06-04 18:12:33.251245", 43 | "modified_by": "Administrator", 44 | "module": null, 45 | "name": "Tax Category-custom_category_reason", 46 | "no_copy": 0, 47 | "non_negative": 0, 48 | "options": null, 49 | "owner": "Administrator", 50 | "permlevel": 0, 51 | "precision": "", 52 | "print_hide": 0, 53 | "print_hide_if_no_value": 0, 54 | "print_width": null, 55 | "read_only": 0, 56 | "read_only_depends_on": null, 57 | "report_hide": 0, 58 | "reqd": 0, 59 | "search_index": 0, 60 | "show_dashboard": 0, 61 | "sort_options": 0, 62 | "translatable": 1, 63 | "unique": 0, 64 | "width": null 65 | }, 66 | { 67 | "_assign": null, 68 | "_comments": null, 69 | "_liked_by": null, 70 | "_user_tags": null, 71 | "allow_in_quick_entry": 0, 72 | "allow_on_submit": 0, 73 | "bold": 0, 74 | "collapsible": 0, 75 | "collapsible_depends_on": null, 76 | "columns": 0, 77 | "creation": "2024-06-04 18:03:18.543757", 78 | "default": null, 79 | "depends_on": null, 80 | "description": null, 81 | "docstatus": 0, 82 | "dt": "Tax Category", 83 | "fetch_from": null, 84 | "fetch_if_empty": 0, 85 | "fieldname": "custom_zatca_category", 86 | "fieldtype": "Select", 87 | "hidden": 0, 88 | "hide_border": 0, 89 | "hide_days": 0, 90 | "hide_seconds": 0, 91 | "idx": 4, 92 | "ignore_user_permissions": 0, 93 | "ignore_xss_filter": 0, 94 | "in_global_search": 0, 95 | "in_list_view": 0, 96 | "in_preview": 0, 97 | "in_standard_filter": 0, 98 | "insert_after": "custom_column_break_680fq", 99 | "is_system_generated": 0, 100 | "is_virtual": 0, 101 | "label": "ZATCA Category", 102 | "length": 0, 103 | "link_filters": null, 104 | "mandatory_depends_on": null, 105 | "modified": "2024-06-04 18:03:18.543757", 106 | "modified_by": "Administrator", 107 | "module": null, 108 | "name": "Tax Category-custom_zatca_category", 109 | "no_copy": 0, 110 | "non_negative": 0, 111 | "options": "\nStandard rate\nServices outside scope of tax / Not subject to VAT || {manual entry}\nExempt from Tax || Financial services mentioned in Article 29 of the VAT Regulations\nExempt from Tax || Life insurance services mentioned in Article 29 of the VAT Regulations\nExempt from Tax || Real estate transactions mentioned in Article 30 of the VAT Regulations\nExempt from Tax || Qualified Supply of Goods in Duty Free area\nZero rated goods || Export of goods\nZero rated goods || Export of services\nZero rated goods || The international transport of Goods\nZero rated goods || International transport of passengers\nZero rated goods || Services directly connected and incidental to a Supply of international passenger transport\nZero rated goods || Supply of a qualifying means of transport\nZero rated goods || Any services relating to Goods or passenger transportation as defined in article twenty five of these Regulations\nZero rated goods || Medicines and medical equipment\nZero rated goods || Qualifying metals\nZero rated goods || Private education to citizen\nZero rated goods || Private healthcare to citizen\nZero rated goods || Supply of qualified military goods", 112 | "owner": "Administrator", 113 | "permlevel": 0, 114 | "precision": "", 115 | "print_hide": 0, 116 | "print_hide_if_no_value": 0, 117 | "print_width": null, 118 | "read_only": 0, 119 | "read_only_depends_on": null, 120 | "report_hide": 0, 121 | "reqd": 0, 122 | "search_index": 0, 123 | "show_dashboard": 0, 124 | "sort_options": 0, 125 | "translatable": 1, 126 | "unique": 0, 127 | "width": null 128 | }, 129 | { 130 | "_assign": null, 131 | "_comments": null, 132 | "_liked_by": null, 133 | "_user_tags": null, 134 | "allow_in_quick_entry": 0, 135 | "allow_on_submit": 0, 136 | "bold": 0, 137 | "collapsible": 0, 138 | "collapsible_depends_on": null, 139 | "columns": 0, 140 | "creation": "2024-06-04 18:03:18.383087", 141 | "default": null, 142 | "depends_on": null, 143 | "description": null, 144 | "docstatus": 0, 145 | "dt": "Tax Category", 146 | "fetch_from": null, 147 | "fetch_if_empty": 0, 148 | "fieldname": "custom_column_break_680fq", 149 | "fieldtype": "Column Break", 150 | "hidden": 0, 151 | "hide_border": 0, 152 | "hide_days": 0, 153 | "hide_seconds": 0, 154 | "idx": 3, 155 | "ignore_user_permissions": 0, 156 | "ignore_xss_filter": 0, 157 | "in_global_search": 0, 158 | "in_list_view": 0, 159 | "in_preview": 0, 160 | "in_standard_filter": 0, 161 | "insert_after": "disabled", 162 | "is_system_generated": 0, 163 | "is_virtual": 0, 164 | "label": "", 165 | "length": 0, 166 | "link_filters": null, 167 | "mandatory_depends_on": null, 168 | "modified": "2024-06-04 18:03:18.383087", 169 | "modified_by": "Administrator", 170 | "module": null, 171 | "name": "Tax Category-custom_column_break_680fq", 172 | "no_copy": 0, 173 | "non_negative": 0, 174 | "options": null, 175 | "owner": "Administrator", 176 | "permlevel": 0, 177 | "precision": "", 178 | "print_hide": 0, 179 | "print_hide_if_no_value": 0, 180 | "print_width": null, 181 | "read_only": 0, 182 | "read_only_depends_on": null, 183 | "report_hide": 0, 184 | "reqd": 0, 185 | "search_index": 0, 186 | "show_dashboard": 0, 187 | "sort_options": 0, 188 | "translatable": 0, 189 | "unique": 0, 190 | "width": null 191 | } 192 | ], 193 | "custom_perms": [], 194 | "doctype": "Tax Category", 195 | "links": [], 196 | "property_setters": [ 197 | { 198 | "_assign": null, 199 | "_comments": null, 200 | "_liked_by": null, 201 | "_user_tags": null, 202 | "creation": "2024-06-05 11:41:15.716803", 203 | "default_value": null, 204 | "doc_type": "Tax Category", 205 | "docstatus": 0, 206 | "doctype_or_field": "DocType", 207 | "field_name": null, 208 | "idx": 0, 209 | "is_system_generated": 0, 210 | "modified": "2024-06-09 12:38:52.072569", 211 | "modified_by": "Administrator", 212 | "module": null, 213 | "name": "Tax Category-main-field_order", 214 | "owner": "Administrator", 215 | "property": "field_order", 216 | "property_type": "Data", 217 | "row_name": null, 218 | "value": "[\"title\", \"disabled\", \"custom_column_break_680fq\", \"custom_zatca_category\", \"custom_category_reason\"]" 219 | } 220 | ], 221 | "sync_on_migrate": 1 222 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/dashboard_chart/integration_status/integration_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "based_on": "", 3 | "chart_name": "Integration Status", 4 | "chart_type": "Group By", 5 | "creation": "2024-05-29 17:28:56.443032", 6 | "docstatus": 0, 7 | "doctype": "Dashboard Chart", 8 | "document_type": "ZATCA Integration Log", 9 | "dynamic_filters_json": "[]", 10 | "filters_json": "[]", 11 | "group_by_based_on": "status", 12 | "group_by_type": "Count", 13 | "idx": 0, 14 | "is_public": 1, 15 | "is_standard": 1, 16 | "last_synced_on": "2024-05-29 17:34:59.083435", 17 | "modified": "2024-05-29 17:40:38.015281", 18 | "modified_by": "Administrator", 19 | "module": "KSA Compliance", 20 | "name": "Integration Status", 21 | "number_of_groups": 0, 22 | "owner": "Administrator", 23 | "parent_document_type": "", 24 | "roles": [ 25 | { 26 | "role": "System Manager" 27 | }, 28 | { 29 | "role": "Accounts Manager" 30 | }, 31 | { 32 | "role": "Sales Manager" 33 | } 34 | ], 35 | "source": "", 36 | "time_interval": "Yearly", 37 | "timeseries": 0, 38 | "timespan": "Last Year", 39 | "type": "Pie", 40 | "use_report_chart": 0, 41 | "value_based_on": "", 42 | "y_axis": [] 43 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/dashboard_chart/invoice_integration_statistics/invoice_integration_statistics.json: -------------------------------------------------------------------------------- 1 | { 2 | "based_on": "", 3 | "chart_name": "Invoice Integration Statistics", 4 | "chart_type": "Group By", 5 | "creation": "2024-05-29 17:13:56.517546", 6 | "docstatus": 0, 7 | "doctype": "Dashboard Chart", 8 | "document_type": "Sales Invoice Additional Fields", 9 | "dynamic_filters_json": "[]", 10 | "filters_json": "[[\"Sales Invoice Additional Fields\",\"is_latest\",\"=\",1,false]]", 11 | "group_by_based_on": "integration_status", 12 | "group_by_type": "Count", 13 | "idx": 0, 14 | "is_public": 1, 15 | "is_standard": 1, 16 | "last_synced_on": "2024-07-03 15:54:36.490966", 17 | "modified": "2024-07-08 12:12:06.997303", 18 | "modified_by": "Administrator", 19 | "module": "KSA Compliance", 20 | "name": "Invoice Integration Statistics", 21 | "number_of_groups": 0, 22 | "owner": "Administrator", 23 | "parent_document_type": "", 24 | "roles": [ 25 | { 26 | "role": "Accounts Manager" 27 | }, 28 | { 29 | "role": "Sales Manager" 30 | } 31 | ], 32 | "source": "", 33 | "time_interval": "Yearly", 34 | "timeseries": 0, 35 | "timespan": "Last Year", 36 | "type": "Donut", 37 | "use_report_chart": 0, 38 | "value_based_on": "", 39 | "y_axis": [] 40 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/additional_buyer_ids/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/additional_buyer_ids/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/additional_buyer_ids/additional_buyer_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2024-01-30 11:09:27.781782", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "type_name", 10 | "type_code", 11 | "value" 12 | ], 13 | "fields": [ 14 | { 15 | "fieldname": "type_name", 16 | "fieldtype": "Data", 17 | "in_list_view": 1, 18 | "label": "Type Name", 19 | "read_only": 1 20 | }, 21 | { 22 | "fieldname": "type_code", 23 | "fieldtype": "Data", 24 | "in_list_view": 1, 25 | "label": "Type Code", 26 | "read_only": 1 27 | }, 28 | { 29 | "fieldname": "value", 30 | "fieldtype": "Data", 31 | "in_list_view": 1, 32 | "label": "Value" 33 | } 34 | ], 35 | "index_web_pages_for_search": 1, 36 | "istable": 1, 37 | "links": [], 38 | "modified": "2024-02-18 18:10:17.044245", 39 | "modified_by": "Administrator", 40 | "module": "KSA Compliance", 41 | "name": "Additional Buyer IDs", 42 | "owner": "Administrator", 43 | "permissions": [], 44 | "sort_field": "modified", 45 | "sort_order": "DESC", 46 | "states": [] 47 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/additional_buyer_ids/additional_buyer_ids.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class AdditionalBuyerIDs(Document): 9 | # begin: auto-generated types 10 | # This code is auto-generated. Do not modify anything in this block. 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from frappe.types import DF 16 | 17 | parent: DF.Data 18 | parentfield: DF.Data 19 | parenttype: DF.Data 20 | type_code: DF.Data | None 21 | type_name: DF.Data | None 22 | value: DF.Data | None 23 | # end: auto-generated types 24 | pass 25 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/additional_seller_ids/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/additional_seller_ids/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/additional_seller_ids/additional_seller_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2024-01-11 16:02:59.223150", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "type_name", 10 | "type_code", 11 | "value" 12 | ], 13 | "fields": [ 14 | { 15 | "fieldname": "type_name", 16 | "fieldtype": "Data", 17 | "in_list_view": 1, 18 | "label": "Type Name", 19 | "read_only": 1 20 | }, 21 | { 22 | "fetch_from": "type_name.type_code", 23 | "fieldname": "type_code", 24 | "fieldtype": "Data", 25 | "in_list_view": 1, 26 | "label": "Type Code", 27 | "read_only": 1 28 | }, 29 | { 30 | "fieldname": "value", 31 | "fieldtype": "Data", 32 | "in_list_view": 1, 33 | "label": "Value" 34 | } 35 | ], 36 | "index_web_pages_for_search": 1, 37 | "istable": 1, 38 | "links": [], 39 | "modified": "2024-02-18 18:09:53.723967", 40 | "modified_by": "Administrator", 41 | "module": "KSA Compliance", 42 | "name": "Additional Seller IDs", 43 | "owner": "Administrator", 44 | "permissions": [], 45 | "sort_field": "modified", 46 | "sort_order": "DESC", 47 | "states": [] 48 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/additional_seller_ids/additional_seller_ids.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class AdditionalSellerIDs(Document): 9 | # begin: auto-generated types 10 | # This code is auto-generated. Do not modify anything in this block. 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from frappe.types import DF 16 | 17 | parent: DF.Data 18 | parentfield: DF.Data 19 | parenttype: DF.Data 20 | type_code: DF.Data | None 21 | type_name: DF.Data | None 22 | value: DF.Data | None 23 | # end: auto-generated types 24 | pass 25 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/registration_type/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/registration_type/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/registration_type/registration_type.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, Lavaloon and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("Registration Type", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/registration_type/registration_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "field:type_name", 5 | "creation": "2024-01-11 16:07:29.095748", 6 | "doctype": "DocType", 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "type_name", 10 | "type_code" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "type_name", 15 | "fieldtype": "Data", 16 | "label": "Type Name", 17 | "unique": 1 18 | }, 19 | { 20 | "fieldname": "type_code", 21 | "fieldtype": "Data", 22 | "label": "Type Code" 23 | } 24 | ], 25 | "index_web_pages_for_search": 1, 26 | "links": [], 27 | "modified": "2024-02-18 18:10:03.290767", 28 | "modified_by": "Administrator", 29 | "module": "KSA Compliance", 30 | "name": "Registration Type", 31 | "naming_rule": "Expression (old style)", 32 | "owner": "Administrator", 33 | "permissions": [ 34 | { 35 | "create": 1, 36 | "delete": 1, 37 | "email": 1, 38 | "export": 1, 39 | "print": 1, 40 | "read": 1, 41 | "report": 1, 42 | "role": "System Manager", 43 | "share": 1, 44 | "write": 1 45 | } 46 | ], 47 | "sort_field": "modified", 48 | "sort_order": "DESC", 49 | "states": [] 50 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/registration_type/registration_type.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class RegistrationType(Document): 9 | # begin: auto-generated types 10 | # This code is auto-generated. Do not modify anything in this block. 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from frappe.types import DF 16 | 17 | type_code: DF.Data | None 18 | type_name: DF.Data | None 19 | # end: auto-generated types 20 | pass 21 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/registration_type/test_registration_type.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestRegistrationType(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/sales_invoice_additional_fields/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/sales_invoice_additional_fields/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/sales_invoice_additional_fields/sales_invoice_additional_fields.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, Lavaloon and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Sales Invoice Additional Fields", { 5 | refresh: function (frm) { 6 | if (frm.doc.integration_status === 'Rejected' && !frm.doc.precomputed_invoice && frm.doc.is_latest) { 7 | frm.add_custom_button(__('Fix Rejection'), () => fix_rejection(frm), null, 'primary'); 8 | } 9 | }, 10 | download_xml: function (frm) { 11 | window.open("/api/method/ksa_compliance.ksa_compliance.doctype.sales_invoice_additional_fields.sales_invoice_additional_fields.download_xml?id=" + frm.doc.name); 12 | }, 13 | download_zatca_pdf: async function (frm) { 14 | await frappe.call({ 15 | method: 'ksa_compliance.ksa_compliance.doctype.sales_invoice_additional_fields.sales_invoice_additional_fields.check_pdf_a3b_support', 16 | args: { 17 | id: frm.doc.name, 18 | } 19 | }) 20 | let fields = [ 21 | { 22 | label: 'Print Format', 23 | fieldname: 'print_format', 24 | fieldtype: 'Link', 25 | options: 'Print Format', 26 | reqd: 1, 27 | }, 28 | { 29 | label: 'Language', 30 | fieldname: 'lang', 31 | fieldtype: 'Link', 32 | options: 'Language', 33 | reqd: 1, 34 | } 35 | ]; 36 | frappe.prompt(fields, values => { 37 | let print_format = values.print_format, 38 | lang = values.lang 39 | window.location.assign("/api/method/ksa_compliance.ksa_compliance.doctype.sales_invoice_additional_fields.sales_invoice_additional_fields.download_zatca_pdf?id=" + frm.doc.name + "&print_format=" + print_format + "&lang=" + lang); 40 | }) 41 | } 42 | }); 43 | 44 | async function fix_rejection(frm) { 45 | let invoice_link = `${frm.doc.sales_invoice}` 46 | let message = __("

This will create a new Sales Invoice Additional Fields document for the invoice '{0}' and " + 47 | "submit it to ZATCA. Make sure you have updated any bad configuration that lead to the initial rejection.

" + 48 | "

Do you want to proceed?

", [invoice_link]) 49 | frappe.confirm(message, async () => { 50 | await frappe.call({ 51 | freeze: true, 52 | freeze_message: __('Please wait...'), 53 | method: "ksa_compliance.ksa_compliance.doctype.sales_invoice_additional_fields.sales_invoice_additional_fields.fix_rejection", 54 | args: { 55 | id: frm.doc.name, 56 | }, 57 | }); 58 | // Reload the document to update the 'Is Latest' flag and hide the fix rejection button if we successfully 59 | // created a new SIAF 60 | frm.reload_doc(); 61 | }, () => { 62 | }); 63 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/sales_invoice_additional_fields/test_sales_invoice_additional_fields.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestSalesInvoiceAdditionalFields(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_business_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/zatca_business_settings/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_business_settings/test_zatca_business_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestZATCABusinessSettings(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_egs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/zatca_egs/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_egs/test_zatca_egs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestZATCAEGS(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_egs/zatca_egs.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, LavaLoon and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("ZATCA EGS", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_egs/zatca_egs.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "hash", 5 | "creation": "2024-03-15 01:37:35.019775", 6 | "default_view": "List", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "tab_break_ljdo", 11 | "business_settings", 12 | "unit_common_name", 13 | "unit_serial", 14 | "enable_zatca_integration", 15 | "sync_with_zatca", 16 | "column_break_kjzc", 17 | "egs_type", 18 | "integration_tab", 19 | "configuration_section", 20 | "validate_generated_xml", 21 | "onboarding_section", 22 | "csr", 23 | "compliance_request_id", 24 | "security_token", 25 | "secret", 26 | "column_break_okfu", 27 | "production_request_id", 28 | "production_security_token", 29 | "production_secret" 30 | ], 31 | "fields": [ 32 | { 33 | "fieldname": "tab_break_ljdo", 34 | "fieldtype": "Tab Break", 35 | "label": "Configuration" 36 | }, 37 | { 38 | "default": "0", 39 | "fieldname": "enable_zatca_integration", 40 | "fieldtype": "Check", 41 | "label": "Enable ZATCA Integration" 42 | }, 43 | { 44 | "fieldname": "sync_with_zatca", 45 | "fieldtype": "Select", 46 | "label": "Sync with ZATCA", 47 | "options": "Live\nBatches" 48 | }, 49 | { 50 | "fieldname": "column_break_kjzc", 51 | "fieldtype": "Column Break" 52 | }, 53 | { 54 | "fieldname": "integration_tab", 55 | "fieldtype": "Tab Break", 56 | "label": "Integration" 57 | }, 58 | { 59 | "fieldname": "configuration_section", 60 | "fieldtype": "Section Break", 61 | "label": "Configuration" 62 | }, 63 | { 64 | "default": "0", 65 | "description": "If enabled, validates the generated ZATCA XML when creating Sales Invoice Additional Fields. This has a performance impact, so it's recommended to enable only for testing or troubleshooting problems.", 66 | "fieldname": "validate_generated_xml", 67 | "fieldtype": "Check", 68 | "label": "Validate Generated XML" 69 | }, 70 | { 71 | "fieldname": "onboarding_section", 72 | "fieldtype": "Section Break", 73 | "label": "Onboarding" 74 | }, 75 | { 76 | "fieldname": "csr", 77 | "fieldtype": "Small Text", 78 | "label": "CSR", 79 | "read_only": 1 80 | }, 81 | { 82 | "fieldname": "compliance_request_id", 83 | "fieldtype": "Data", 84 | "label": "Compliance Request ID", 85 | "read_only": 1 86 | }, 87 | { 88 | "fieldname": "security_token", 89 | "fieldtype": "Small Text", 90 | "label": "Security Token", 91 | "read_only": 1 92 | }, 93 | { 94 | "fieldname": "secret", 95 | "fieldtype": "Password", 96 | "label": "Secret", 97 | "read_only": 1 98 | }, 99 | { 100 | "fieldname": "column_break_okfu", 101 | "fieldtype": "Column Break" 102 | }, 103 | { 104 | "fieldname": "production_request_id", 105 | "fieldtype": "Data", 106 | "label": "Production Request ID", 107 | "read_only": 1 108 | }, 109 | { 110 | "fieldname": "production_security_token", 111 | "fieldtype": "Small Text", 112 | "label": "Production Security Token", 113 | "read_only": 1 114 | }, 115 | { 116 | "fieldname": "production_secret", 117 | "fieldtype": "Password", 118 | "label": "Production Secret", 119 | "read_only": 1 120 | }, 121 | { 122 | "fieldname": "business_settings", 123 | "fieldtype": "Link", 124 | "in_standard_filter": 1, 125 | "label": "Business Settings", 126 | "options": "ZATCA Business Settings", 127 | "reqd": 1 128 | }, 129 | { 130 | "fieldname": "egs_type", 131 | "fieldtype": "Select", 132 | "in_list_view": 1, 133 | "in_standard_filter": 1, 134 | "label": "EGS Type", 135 | "options": "ERPNext\nPOS Device", 136 | "reqd": 1 137 | }, 138 | { 139 | "description": "EGS/device identifier. For ERPNext, enter \"ERPNext\" or similar. For POS devices, enter the POS device ID shown in LavaDo.
\nFrom the home screen: Settings (gear button top-left/right) -> More Info and look for the device ID", 140 | "fieldname": "unit_common_name", 141 | "fieldtype": "Data", 142 | "in_list_view": 1, 143 | "in_standard_filter": 1, 144 | "label": "Unit Common Name", 145 | "reqd": 1 146 | }, 147 | { 148 | "description": "Format:
1-Solution Provider Name|2-Model or version|3-Serial
\n
\nExample:
1-ERPNext|2-15|3-1
or
1-Sunmi|2-Sunmi Pro v2s|3-V222201B00815
", 149 | "fetch_from": "business_settings.company_unit_serial", 150 | "fetch_if_empty": 1, 151 | "fieldname": "unit_serial", 152 | "fieldtype": "Data", 153 | "in_list_view": 1, 154 | "label": "Unit Serial", 155 | "reqd": 1 156 | } 157 | ], 158 | "index_web_pages_for_search": 1, 159 | "links": [], 160 | "modified": "2024-03-18 03:34:11.904323", 161 | "modified_by": "Administrator", 162 | "module": "KSA Compliance", 163 | "name": "ZATCA EGS", 164 | "naming_rule": "Random", 165 | "owner": "Administrator", 166 | "permissions": [ 167 | { 168 | "create": 1, 169 | "delete": 1, 170 | "email": 1, 171 | "export": 1, 172 | "print": 1, 173 | "read": 1, 174 | "report": 1, 175 | "role": "System Manager", 176 | "share": 1, 177 | "write": 1 178 | } 179 | ], 180 | "sort_field": "modified", 181 | "sort_order": "DESC", 182 | "states": [], 183 | "track_changes": 1 184 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_egs/zatca_egs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and contributors 2 | # For license information, please see license.txt 3 | from typing import cast 4 | 5 | import frappe 6 | from frappe import _ 7 | from frappe.model.document import Document 8 | 9 | 10 | class ZATCAEGS(Document): 11 | # begin: auto-generated types 12 | # This code is auto-generated. Do not modify anything in this block. 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | if TYPE_CHECKING: 17 | from frappe.types import DF 18 | 19 | business_settings: DF.Link 20 | compliance_request_id: DF.Data | None 21 | csr: DF.SmallText | None 22 | egs_type: DF.Literal['ERPNext', 'POS Device'] 23 | enable_zatca_integration: DF.Check 24 | production_request_id: DF.Data | None 25 | production_secret: DF.Password | None 26 | production_security_token: DF.SmallText | None 27 | secret: DF.Password | None 28 | security_token: DF.SmallText | None 29 | sync_with_zatca: DF.Literal['Live', 'Batches'] 30 | unit_common_name: DF.Data 31 | unit_serial: DF.Data 32 | validate_generated_xml: DF.Check 33 | # end: auto-generated types 34 | 35 | @property 36 | def is_live_sync(self) -> bool: 37 | return self.sync_with_zatca.lower() == 'live' 38 | 39 | @staticmethod 40 | def for_device(device_id: str) -> 'ZATCAEGS | None': 41 | egs = cast(str | None, frappe.db.exists('ZATCA EGS', {'unit_common_name': device_id})) 42 | if egs: 43 | return cast(ZATCAEGS, frappe.get_doc('ZATCA EGS', egs)) 44 | 45 | return None 46 | 47 | def on_trash(self) -> None: 48 | frappe.throw(msg=_('You cannot Delete a configured ZATCA EGS'), title=_('This Action Is Not Allowed')) 49 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_integration_log/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/zatca_integration_log/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_integration_log/test_zatca_integration_log.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestZATCAIntegrationLog(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_integration_log/zatca_integration_log.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, Lavaloon and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("ZATCA Integration Log", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_integration_log/zatca_integration_log.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2024-01-29 14:36:22.120690", 5 | "doctype": "DocType", 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "invoice_doctype", 9 | "invoice_reference", 10 | "column_break_zzuv", 11 | "invoice_additional_fields_reference", 12 | "section_break_iwip", 13 | "e_invoice_file", 14 | "status", 15 | "zatca_status", 16 | "resend", 17 | "zatca_message" 18 | ], 19 | "fields": [ 20 | { 21 | "fieldname": "invoice_reference", 22 | "fieldtype": "Dynamic Link", 23 | "label": "Invoice Reference", 24 | "options": "invoice_doctype", 25 | "read_only": 1, 26 | "reqd": 1, 27 | "search_index": 1 28 | }, 29 | { 30 | "fieldname": "column_break_zzuv", 31 | "fieldtype": "Column Break" 32 | }, 33 | { 34 | "fieldname": "invoice_additional_fields_reference", 35 | "fieldtype": "Link", 36 | "label": "Invoice Additional Fields Reference", 37 | "options": "Sales Invoice Additional Fields", 38 | "read_only": 1, 39 | "reqd": 1, 40 | "search_index": 1 41 | }, 42 | { 43 | "fieldname": "section_break_iwip", 44 | "fieldtype": "Section Break" 45 | }, 46 | { 47 | "fieldname": "e_invoice_file", 48 | "fieldtype": "Attach", 49 | "label": "E-invoice File", 50 | "read_only": 1 51 | }, 52 | { 53 | "default": "Pending", 54 | "fieldname": "status", 55 | "fieldtype": "Select", 56 | "in_list_view": 1, 57 | "label": "Status", 58 | "options": "\nPending\nResend\nAccepted with warnings\nAccepted\nRejected\nClearance switched off", 59 | "read_only": 1 60 | }, 61 | { 62 | "fieldname": "zatca_status", 63 | "fieldtype": "Data", 64 | "in_list_view": 1, 65 | "label": "ZATCA Status", 66 | "read_only": 1 67 | }, 68 | { 69 | "fieldname": "zatca_message", 70 | "fieldtype": "Long Text", 71 | "label": "ZATCA Message", 72 | "read_only": 1 73 | }, 74 | { 75 | "depends_on": "eval:doc.status == \"Resend\";", 76 | "fieldname": "resend", 77 | "fieldtype": "Button", 78 | "label": "Resend" 79 | }, 80 | { 81 | "default": "Sales Invoice", 82 | "fieldname": "invoice_doctype", 83 | "fieldtype": "Select", 84 | "in_standard_filter": 1, 85 | "label": "Invoice Doctype", 86 | "options": "Sales Invoice\nPOS Invoice", 87 | "read_only": 1, 88 | "reqd": 1, 89 | "search_index": 1 90 | } 91 | ], 92 | "index_web_pages_for_search": 1, 93 | "links": [], 94 | "modified": "2024-09-04 13:13:24.391032", 95 | "modified_by": "Administrator", 96 | "module": "KSA Compliance", 97 | "name": "ZATCA Integration Log", 98 | "owner": "Administrator", 99 | "permissions": [ 100 | { 101 | "create": 1, 102 | "delete": 1, 103 | "email": 1, 104 | "export": 1, 105 | "print": 1, 106 | "read": 1, 107 | "report": 1, 108 | "role": "System Manager", 109 | "share": 1, 110 | "write": 1 111 | } 112 | ], 113 | "sort_field": "modified", 114 | "sort_order": "DESC", 115 | "states": [] 116 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_integration_log/zatca_integration_log.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class ZATCAIntegrationLog(Document): 9 | # begin: auto-generated types 10 | # This code is auto-generated. Do not modify anything in this block. 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from frappe.types import DF 16 | 17 | e_invoice_file: DF.Attach | None 18 | invoice_additional_fields_reference: DF.Link 19 | invoice_doctype: DF.Literal['Sales Invoice', 'POS Invoice'] 20 | invoice_reference: DF.DynamicLink 21 | status: DF.Literal[ 22 | '', 'Pending', 'Resend', 'Accepted with warnings', 'Accepted', 'Rejected', 'Clearance switched off' 23 | ] 24 | zatca_message: DF.LongText | None 25 | zatca_status: DF.Data | None 26 | # end: auto-generated types 27 | pass 28 | 29 | def autoname(self): 30 | count = len(frappe.get_all(self.doctype, {'invoice_reference': self.invoice_reference}, pluck='name')) 31 | self.name = f'log-{self.invoice_reference}-{count + 1}' 32 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_invoice_counting_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/zatca_invoice_counting_settings/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_invoice_counting_settings/test_zatca_invoice_counting_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestZATCAInvoiceCountingSettings(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_invoice_counting_settings/zatca_invoice_counting_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, LavaLoon and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("ZATCA Invoice Counting Settings", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_invoice_counting_settings/zatca_invoice_counting_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "field:business_settings_reference", 5 | "creation": "2024-02-27 16:43:30.925017", 6 | "doctype": "DocType", 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "business_settings_reference", 10 | "zatca_egs", 11 | "column_break_ptqh", 12 | "invoice_counter", 13 | "previous_invoice_hash" 14 | ], 15 | "fields": [ 16 | { 17 | "description": "Business Settings ID Reference", 18 | "fieldname": "business_settings_reference", 19 | "fieldtype": "Link", 20 | "in_list_view": 1, 21 | "label": "ZATCA Business Settings Reference", 22 | "options": "ZATCA Business Settings", 23 | "set_only_once": 1, 24 | "unique": 1 25 | }, 26 | { 27 | "fieldname": "column_break_ptqh", 28 | "fieldtype": "Column Break" 29 | }, 30 | { 31 | "default": "0", 32 | "description": "Number of invoices for each business settings doc.", 33 | "fieldname": "invoice_counter", 34 | "fieldtype": "Int", 35 | "in_list_view": 1, 36 | "label": "Invoice Counter", 37 | "non_negative": 1, 38 | "read_only": 1 39 | }, 40 | { 41 | "description": "Previous invoice hash for each business settings doc", 42 | "fieldname": "previous_invoice_hash", 43 | "fieldtype": "Data", 44 | "label": "Previous Invoice Hash", 45 | "read_only": 1 46 | }, 47 | { 48 | "fieldname": "zatca_egs", 49 | "fieldtype": "Link", 50 | "label": "ZATCA EGS", 51 | "options": "ZATCA EGS" 52 | } 53 | ], 54 | "index_web_pages_for_search": 1, 55 | "links": [], 56 | "modified": "2024-03-15 02:05:51.696912", 57 | "modified_by": "Administrator", 58 | "module": "KSA Compliance", 59 | "name": "ZATCA Invoice Counting Settings", 60 | "naming_rule": "Expression", 61 | "owner": "Administrator", 62 | "permissions": [ 63 | { 64 | "create": 1, 65 | "delete": 1, 66 | "email": 1, 67 | "export": 1, 68 | "print": 1, 69 | "read": 1, 70 | "report": 1, 71 | "role": "System Manager", 72 | "share": 1, 73 | "write": 1 74 | } 75 | ], 76 | "sort_field": "modified", 77 | "sort_order": "DESC", 78 | "states": [], 79 | "track_changes": 1 80 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_invoice_counting_settings/zatca_invoice_counting_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe import _ 6 | from frappe.model.document import Document 7 | 8 | 9 | class ZATCAInvoiceCountingSettings(Document): 10 | # begin: auto-generated types 11 | # This code is auto-generated. Do not modify anything in this block. 12 | 13 | from typing import TYPE_CHECKING 14 | 15 | if TYPE_CHECKING: 16 | from frappe.types import DF 17 | 18 | business_settings_reference: DF.Link | None 19 | invoice_counter: DF.Int 20 | previous_invoice_hash: DF.Data | None 21 | zatca_egs: DF.Link | None 22 | # end: auto-generated types 23 | 24 | def on_trash(self) -> None: 25 | frappe.throw( 26 | msg=_('You cannot delete a configured Invoice Counting Settings'), title=_('This Action Is Not Allowed') 27 | ) 28 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_phase_1_business_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/zatca_phase_1_business_settings/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_phase_1_business_settings/test_zatca_phase_1_business_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestZATCAPhase1BusinessSettings(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_phase_1_business_settings/zatca_phase_1_business_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, LavaLoon and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("ZATCA Phase 1 Business Settings", { 5 | company(frm) { 6 | fetch_company_primary_address(frm) 7 | filter_address_by_company(frm) 8 | }, 9 | }); 10 | 11 | 12 | function fetch_company_primary_address(frm) { 13 | frappe.call({ 14 | method: "ksa_compliance.ksa_compliance.doctype.zatca_phase_1_business_settings.zatca_phase_1_business_settings.get_company_primary_address", 15 | args: { 16 | company: frm.doc.company, 17 | }, 18 | callback: function (r) { 19 | if (r.message.length === 0) { 20 | frm.set_value("address", "") 21 | } else { 22 | frm.set_value("address", r.message[0].name) 23 | } 24 | } 25 | }); 26 | } 27 | 28 | function filter_address_by_company(frm) { 29 | frappe.call({ 30 | method: "ksa_compliance.ksa_compliance.doctype.zatca_phase_1_business_settings.zatca_phase_1_business_settings.get_all_company_addresses", 31 | args: { 32 | company: frm.doc.company 33 | }, 34 | callback: function (r) { 35 | if (r.message) { 36 | frm.set_query("address", function () { 37 | return { 38 | filters: { 39 | name: ["in", r.message] 40 | } 41 | } 42 | }) 43 | } 44 | } 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_phase_1_business_settings/zatca_phase_1_business_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "field:company", 5 | "creation": "2024-05-23 15:10:35.449147", 6 | "doctype": "DocType", 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "company", 10 | "vat_registration_number", 11 | "type_of_transaction", 12 | "column_break_gigi", 13 | "address", 14 | "status" 15 | ], 16 | "fields": [ 17 | { 18 | "fieldname": "company", 19 | "fieldtype": "Link", 20 | "in_list_view": 1, 21 | "label": "Company", 22 | "options": "Company", 23 | "reqd": 1, 24 | "unique": 1 25 | }, 26 | { 27 | "fieldname": "vat_registration_number", 28 | "fieldtype": "Data", 29 | "label": "VAT Registration Number" 30 | }, 31 | { 32 | "fieldname": "type_of_transaction", 33 | "fieldtype": "Select", 34 | "label": "Type Of Transaction", 35 | "options": "Simplified Tax Invoice\nStandard Tax Invoice\nBoth" 36 | }, 37 | { 38 | "fieldname": "column_break_gigi", 39 | "fieldtype": "Column Break" 40 | }, 41 | { 42 | "fieldname": "address", 43 | "fieldtype": "Link", 44 | "in_list_view": 1, 45 | "label": "Address", 46 | "options": "Address", 47 | "reqd": 1 48 | }, 49 | { 50 | "fieldname": "status", 51 | "fieldtype": "Select", 52 | "label": "Status", 53 | "options": "Active\nDisabled" 54 | } 55 | ], 56 | "index_web_pages_for_search": 1, 57 | "links": [], 58 | "modified": "2024-06-12 14:35:12.719923", 59 | "modified_by": "Administrator", 60 | "module": "KSA Compliance", 61 | "name": "ZATCA Phase 1 Business Settings", 62 | "naming_rule": "By fieldname", 63 | "owner": "Administrator", 64 | "permissions": [ 65 | { 66 | "create": 1, 67 | "delete": 1, 68 | "email": 1, 69 | "export": 1, 70 | "print": 1, 71 | "read": 1, 72 | "report": 1, 73 | "role": "System Manager", 74 | "share": 1, 75 | "write": 1 76 | }, 77 | { 78 | "create": 1, 79 | "delete": 1, 80 | "email": 1, 81 | "export": 1, 82 | "print": 1, 83 | "read": 1, 84 | "report": 1, 85 | "role": "Administrator", 86 | "share": 1, 87 | "write": 1 88 | } 89 | ], 90 | "sort_field": "modified", 91 | "sort_order": "DESC", 92 | "states": [], 93 | "title_field": "company" 94 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_phase_1_business_settings/zatca_phase_1_business_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | from frappe.utils.data import get_link_to_form 7 | from frappe.query_builder import DocType 8 | 9 | 10 | class ZATCAPhase1BusinessSettings(Document): 11 | # begin: auto-generated types 12 | # This code is auto-generated. Do not modify anything in this block. 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | if TYPE_CHECKING: 17 | from frappe.types import DF 18 | 19 | address: DF.Link 20 | company: DF.Link 21 | status: DF.Literal['Active', 'Disabled'] 22 | type_of_transaction: DF.Literal['Simplified Tax Invoice', 'Standard Tax Invoice', 'Both'] 23 | vat_registration_number: DF.Data | None 24 | # end: auto-generated types 25 | pass 26 | 27 | def validate(self): 28 | business_settings_id = frappe.get_value('ZATCA Business Settings', {'company': self.company}) 29 | if business_settings_id and self.status == 'Active': 30 | link = get_link_to_form('ZATCA Business Settings', business_settings_id) 31 | frappe.throw( 32 | f'ZATCA Phase 2 Business Settings already enabled for company {self.company}: {link}', 33 | title='Another Setting Already Enabled', 34 | ) 35 | 36 | @staticmethod 37 | def is_enabled_for_company(company_id: str) -> bool: 38 | return bool( 39 | frappe.db.get_value('ZATCA Phase 1 Business Settings', filters={'company': company_id, 'status': 'Active'}) 40 | ) 41 | 42 | 43 | @frappe.whitelist() 44 | def get_company_primary_address(company): 45 | dynamic_link = DocType('Dynamic Link') 46 | address = DocType('Address') 47 | query = ( 48 | frappe.qb.from_(address) 49 | .select(address.name) 50 | .where(address.is_primary_address == 1) 51 | .join(dynamic_link) 52 | .on(address.name == dynamic_link.parent) 53 | .where(dynamic_link.link_name == company) 54 | ).run(as_dict=1) 55 | return query 56 | 57 | 58 | @frappe.whitelist() 59 | def get_all_company_addresses(company): 60 | return frappe.get_all( 61 | 'Dynamic Link', filters={'link_name': company, 'parenttype': 'Address'}, fields=['parent'], pluck='parent' 62 | ) 63 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_precomputed_invoice/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/zatca_precomputed_invoice/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_precomputed_invoice/test_zatca_precomputed_invoice.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestZATCAPrecomputedInvoice(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_precomputed_invoice/zatca_precomputed_invoice.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, LavaLoon and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("ZATCA Precomputed Invoice", { 5 | download_xml: function (frm) { 6 | window.open("/api/method/ksa_compliance.ksa_compliance.doctype.zatca_precomputed_invoice.zatca_precomputed_invoice.download_xml?id=" + frm.doc.name); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_precomputed_invoice/zatca_precomputed_invoice.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2024-03-18 02:56:06.239025", 5 | "doctype": "DocType", 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "device_id", 9 | "sales_invoice", 10 | "invoice_counter", 11 | "invoice_uuid", 12 | "download_xml", 13 | "column_break_isjc", 14 | "previous_invoice_hash", 15 | "invoice_hash", 16 | "invoice_qr", 17 | "invoice_xml" 18 | ], 19 | "fields": [ 20 | { 21 | "fieldname": "sales_invoice", 22 | "fieldtype": "Link", 23 | "in_list_view": 1, 24 | "in_standard_filter": 1, 25 | "label": "Sales Invoice", 26 | "options": "Sales Invoice", 27 | "read_only": 1, 28 | "search_index": 1 29 | }, 30 | { 31 | "fieldname": "invoice_counter", 32 | "fieldtype": "Data", 33 | "in_list_view": 1, 34 | "in_standard_filter": 1, 35 | "label": "Invoice Counter", 36 | "read_only": 1, 37 | "search_index": 1 38 | }, 39 | { 40 | "fieldname": "invoice_uuid", 41 | "fieldtype": "Data", 42 | "in_list_view": 1, 43 | "in_standard_filter": 1, 44 | "label": "Invoice UUID", 45 | "read_only": 1, 46 | "unique": 1 47 | }, 48 | { 49 | "fieldname": "previous_invoice_hash", 50 | "fieldtype": "Data", 51 | "label": "Previous Invoice Hash", 52 | "read_only": 1 53 | }, 54 | { 55 | "fieldname": "invoice_hash", 56 | "fieldtype": "Data", 57 | "label": "Invoice Hash", 58 | "read_only": 1 59 | }, 60 | { 61 | "fieldname": "invoice_qr", 62 | "fieldtype": "Small Text", 63 | "label": "Invoice QR", 64 | "read_only": 1 65 | }, 66 | { 67 | "fieldname": "invoice_xml", 68 | "fieldtype": "Long Text", 69 | "hidden": 1, 70 | "ignore_xss_filter": 1, 71 | "label": "Invoice XML", 72 | "read_only": 1 73 | }, 74 | { 75 | "fieldname": "download_xml", 76 | "fieldtype": "Button", 77 | "label": "Download XML" 78 | }, 79 | { 80 | "fieldname": "column_break_isjc", 81 | "fieldtype": "Column Break" 82 | }, 83 | { 84 | "fieldname": "device_id", 85 | "fieldtype": "Data", 86 | "label": "Device ID" 87 | } 88 | ], 89 | "index_web_pages_for_search": 1, 90 | "links": [], 91 | "modified": "2024-03-21 08:58:02.883012", 92 | "modified_by": "Administrator", 93 | "module": "KSA Compliance", 94 | "name": "ZATCA Precomputed Invoice", 95 | "owner": "Administrator", 96 | "permissions": [ 97 | { 98 | "create": 1, 99 | "delete": 1, 100 | "email": 1, 101 | "export": 1, 102 | "print": 1, 103 | "read": 1, 104 | "report": 1, 105 | "role": "System Manager", 106 | "share": 1, 107 | "write": 1 108 | } 109 | ], 110 | "sort_field": "modified", 111 | "sort_order": "DESC", 112 | "states": [] 113 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_precomputed_invoice/zatca_precomputed_invoice.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and contributors 2 | # For license information, please see license.txt 3 | from typing import cast, Optional 4 | 5 | import frappe 6 | from frappe import _ 7 | from frappe.model.document import Document 8 | 9 | 10 | class ZATCAPrecomputedInvoice(Document): 11 | # begin: auto-generated types 12 | # This code is auto-generated. Do not modify anything in this block. 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | if TYPE_CHECKING: 17 | from frappe.types import DF 18 | 19 | device_id: DF.Data | None 20 | invoice_counter: DF.Data | None 21 | invoice_hash: DF.Data | None 22 | invoice_qr: DF.SmallText | None 23 | invoice_uuid: DF.Data | None 24 | invoice_xml: DF.LongText | None 25 | previous_invoice_hash: DF.Data | None 26 | sales_invoice: DF.Link | None 27 | # end: auto-generated types 28 | pass 29 | 30 | @staticmethod 31 | def for_invoice(invoice_id: str) -> Optional['ZATCAPrecomputedInvoice']: 32 | prepared_id = frappe.db.exists({'doctype': 'ZATCA Precomputed Invoice', 'sales_invoice': invoice_id}) 33 | if not prepared_id: 34 | return None 35 | 36 | return cast('ZATCAPrecomputedInvoice', frappe.get_doc('ZATCA Precomputed Invoice', prepared_id)) 37 | 38 | def on_trash(self) -> None: 39 | frappe.throw( 40 | msg=_('You cannot Delete a configured ZATCA Precomputed Invoice'), title=_('This Action Is Not Allowed') 41 | ) 42 | 43 | 44 | @frappe.whitelist() 45 | def download_xml(id: str): 46 | """ 47 | Frappe doesn't know how to display an XML field without escaping it, so we made the field hidden. The only way 48 | for users to view the XML is to download it through this endpoint 49 | """ 50 | doc = cast(ZATCAPrecomputedInvoice, frappe.get_doc('ZATCA Precomputed Invoice', id)) 51 | 52 | # Reference: https://frappeframework.com/docs/user/en/python-api/response 53 | frappe.response.filename = doc.name + '.xml' 54 | frappe.response.filecontent = doc.invoice_xml 55 | frappe.response.type = 'download' 56 | frappe.response.display_content_as = 'attachment' 57 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_return_against_reference/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/doctype/zatca_return_against_reference/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_return_against_reference/zatca_return_against_reference.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2025-02-26 12:38:26.077611", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "sales_invoice" 10 | ], 11 | "fields": [ 12 | { 13 | "fieldname": "sales_invoice", 14 | "fieldtype": "Link", 15 | "in_list_view": 1, 16 | "label": "Sales Invoice", 17 | "options": "Sales Invoice", 18 | "reqd": 1 19 | } 20 | ], 21 | "index_web_pages_for_search": 1, 22 | "istable": 1, 23 | "links": [], 24 | "modified": "2025-02-26 13:03:53.548653", 25 | "modified_by": "Administrator", 26 | "module": "KSA Compliance", 27 | "name": "ZATCA Return Against Reference", 28 | "owner": "Administrator", 29 | "permissions": [], 30 | "sort_field": "modified", 31 | "sort_order": "DESC", 32 | "states": [] 33 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/doctype/zatca_return_against_reference/zatca_return_against_reference.py: -------------------------------------------------------------------------------- 1 | from frappe.model.document import Document 2 | 3 | 4 | class ZATCAReturnAgainstReference(Document): 5 | # begin: auto-generated types 6 | # This code is auto-generated. Do not modify anything in this block. 7 | 8 | from typing import TYPE_CHECKING 9 | 10 | if TYPE_CHECKING: 11 | from frappe.types import DF 12 | 13 | parent: DF.Data 14 | parentfield: DF.Data 15 | parenttype: DF.Data 16 | sales_invoice: DF.Link 17 | # end: auto-generated types 18 | pass 19 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/ksa_compliance_dashboard/overall_integration/overall_integration.json: -------------------------------------------------------------------------------- 1 | { 2 | "cards": [ 3 | { 4 | "card": "Accepted Invoices" 5 | }, 6 | { 7 | "card": "Rejected Invoices" 8 | }, 9 | { 10 | "card": "Accepted With Warnings invoices" 11 | }, 12 | { 13 | "card": "Resend Invoices" 14 | }, 15 | { 16 | "card": "Ready For Batch Invoices" 17 | } 18 | ], 19 | "charts": [ 20 | { 21 | "chart": "Integration Status", 22 | "width": "Full" 23 | }, 24 | { 25 | "chart": "Invoice Integration Statistics", 26 | "width": "Full" 27 | } 28 | ], 29 | "creation": "2024-05-29 17:29:49.734729", 30 | "dashboard_name": "OverAll Integration", 31 | "docstatus": 0, 32 | "doctype": "Dashboard", 33 | "idx": 0, 34 | "is_default": 0, 35 | "is_standard": 1, 36 | "modified": "2024-05-29 17:42:14.085299", 37 | "modified_by": "Administrator", 38 | "module": "KSA Compliance", 39 | "name": "OverAll Integration", 40 | "owner": "Administrator" 41 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/number_card/accepted_invoices/accepted_invoices.json: -------------------------------------------------------------------------------- 1 | { 2 | "aggregate_function_based_on": "", 3 | "color": "#29CD42", 4 | "creation": "2024-05-29 17:19:22.260844", 5 | "docstatus": 0, 6 | "doctype": "Number Card", 7 | "document_type": "Sales Invoice Additional Fields", 8 | "dynamic_filters_json": "[]", 9 | "filters_json": "[[\"Sales Invoice Additional Fields\",\"integration_status\",\"=\",\"Accepted\",false]]", 10 | "function": "Count", 11 | "idx": 0, 12 | "is_public": 1, 13 | "is_standard": 1, 14 | "label": "Accepted Invoices", 15 | "modified": "2024-05-29 17:41:49.230106", 16 | "modified_by": "Administrator", 17 | "module": "KSA Compliance", 18 | "name": "Accepted Invoices", 19 | "owner": "Administrator", 20 | "parent_document_type": "", 21 | "report_function": "Sum", 22 | "show_percentage_stats": 1, 23 | "stats_time_interval": "Weekly", 24 | "type": "Document Type" 25 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/number_card/accepted_with_warnings_invoices/accepted_with_warnings_invoices.json: -------------------------------------------------------------------------------- 1 | { 2 | "aggregate_function_based_on": "", 3 | "color": "#e0b165", 4 | "creation": "2024-05-29 17:21:02.419842", 5 | "docstatus": 0, 6 | "doctype": "Number Card", 7 | "document_type": "Sales Invoice Additional Fields", 8 | "dynamic_filters_json": "[]", 9 | "filters_json": "[[\"Sales Invoice Additional Fields\",\"integration_status\",\"=\",\"Accepted with warnings\",false]]", 10 | "function": "Count", 11 | "idx": 0, 12 | "is_public": 1, 13 | "is_standard": 1, 14 | "label": "Accepted With Warnings invoices", 15 | "modified": "2024-05-29 17:41:41.831194", 16 | "modified_by": "Administrator", 17 | "module": "KSA Compliance", 18 | "name": "Accepted With Warnings invoices", 19 | "owner": "Administrator", 20 | "parent_document_type": "", 21 | "report_function": "Sum", 22 | "show_percentage_stats": 1, 23 | "stats_time_interval": "Weekly", 24 | "type": "Document Type" 25 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/number_card/ready_for_batch_invoices/ready_for_batch_invoices.json: -------------------------------------------------------------------------------- 1 | { 2 | "aggregate_function_based_on": "", 3 | "color": "#b3b1b1", 4 | "creation": "2024-05-29 17:22:04.976918", 5 | "docstatus": 0, 6 | "doctype": "Number Card", 7 | "document_type": "Sales Invoice Additional Fields", 8 | "dynamic_filters_json": "[]", 9 | "filters_json": "[[\"Sales Invoice Additional Fields\",\"integration_status\",\"=\",\"Ready For Batch\",false]]", 10 | "function": "Count", 11 | "idx": 0, 12 | "is_public": 1, 13 | "is_standard": 1, 14 | "label": "Ready For Batch Invoices", 15 | "modified": "2024-05-29 17:41:20.886876", 16 | "modified_by": "Administrator", 17 | "module": "KSA Compliance", 18 | "name": "Ready For Batch Invoices", 19 | "owner": "Administrator", 20 | "parent_document_type": "", 21 | "report_function": "Sum", 22 | "show_percentage_stats": 1, 23 | "stats_time_interval": "Weekly", 24 | "type": "Document Type" 25 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/number_card/rejected_invoices/rejected_invoices.json: -------------------------------------------------------------------------------- 1 | { 2 | "aggregate_function_based_on": "", 3 | "color": "#db1d1d", 4 | "creation": "2024-05-29 17:20:22.286543", 5 | "docstatus": 0, 6 | "doctype": "Number Card", 7 | "document_type": "Sales Invoice Additional Fields", 8 | "dynamic_filters_json": "[]", 9 | "filters_json": "[[\"Sales Invoice Additional Fields\",\"integration_status\",\"=\",\"Rejected\",false],[\"Sales Invoice Additional Fields\",\"is_latest\",\"=\",1,false]]", 10 | "function": "Count", 11 | "idx": 0, 12 | "is_public": 1, 13 | "is_standard": 1, 14 | "label": "Rejected Invoices", 15 | "modified": "2024-07-08 12:13:43.816299", 16 | "modified_by": "Administrator", 17 | "module": "KSA Compliance", 18 | "name": "Rejected Invoices", 19 | "owner": "Administrator", 20 | "parent_document_type": "", 21 | "report_function": "Sum", 22 | "show_percentage_stats": 1, 23 | "stats_time_interval": "Weekly", 24 | "type": "Document Type" 25 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/number_card/resend_invoices/resend_invoices.json: -------------------------------------------------------------------------------- 1 | { 2 | "aggregate_function_based_on": "", 3 | "color": "#4F9DD9", 4 | "creation": "2024-05-29 17:21:37.679006", 5 | "docstatus": 0, 6 | "doctype": "Number Card", 7 | "document_type": "Sales Invoice Additional Fields", 8 | "dynamic_filters_json": "[]", 9 | "filters_json": "[[\"Sales Invoice Additional Fields\",\"integration_status\",\"=\",\"Resend\",false]]", 10 | "function": "Count", 11 | "idx": 0, 12 | "is_public": 1, 13 | "is_standard": 1, 14 | "label": "Resend Invoices", 15 | "modified": "2024-05-29 17:41:26.992154", 16 | "modified_by": "Administrator", 17 | "module": "KSA Compliance", 18 | "name": "Resend Invoices", 19 | "owner": "Administrator", 20 | "parent_document_type": "", 21 | "report_function": "Sum", 22 | "show_percentage_stats": 1, 23 | "stats_time_interval": "Weekly", 24 | "type": "Document Type" 25 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/page/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/page/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/page/e_invoicing_sync/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/page/e_invoicing_sync/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/page/e_invoicing_sync/e_invoicing_sync.js: -------------------------------------------------------------------------------- 1 | frappe.pages['e-invoicing-sync'].on_page_load = function(wrapper) { 2 | var me = frappe.ui.make_app_page({ 3 | parent: wrapper, 4 | single_column: true 5 | }); 6 | me.page = wrapper.page; 7 | me.page.set_title(__("Sync Invoices")); 8 | 9 | // Calling Initialization method 10 | let batch_date; 11 | initialize(me, batch_date); 12 | } 13 | 14 | function initialize(page, batch_date) { 15 | // Initialization implementation 16 | let field = page.add_field({ 17 | label : "Batch Date", 18 | fieldtype: "Date", 19 | fieldname: "batch_date", 20 | change() { 21 | batch_date = field.get_value(); 22 | } 23 | }); 24 | let $btn = page.set_primary_action("submit", () => sync_invoices(batch_date)); 25 | } 26 | 27 | function sync_invoices(batch_date) { 28 | // calling server side method to sync the invoices. 29 | let submitBtn = document.getElementsByClassName("btn-sm")[0]; 30 | submitBtn.disabled = true; 31 | if (!batch_date) { 32 | submitBtn.disabled = false; 33 | frappe.throw(__("Select a date first.")); 34 | } 35 | //show_alert with indicator 36 | frappe.show_alert({ 37 | message:__("Start Invoices Syncing...."), 38 | indicator:'green' 39 | }, 3); 40 | frappe.call({ 41 | method: "ksa_compliance.background_jobs.add_batch_to_background_queue", 42 | args: { 43 | "check_date": batch_date 44 | }, 45 | callback: function(r) { 46 | if (!r.exc) { 47 | frappe.msgprint("Syncing End....."); 48 | } 49 | } 50 | }); 51 | submitBtn.disabled = true; 52 | console.log(batch_date); 53 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/page/e_invoicing_sync/e_invoicing_sync.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": null, 3 | "creation": "2024-01-24 15:21:46.916126", 4 | "docstatus": 0, 5 | "doctype": "Page", 6 | "idx": 0, 7 | "modified": "2024-02-29 14:08:56.224384", 8 | "modified_by": "Administrator", 9 | "module": "KSA Compliance", 10 | "name": "e-invoicing-sync", 11 | "owner": "Administrator", 12 | "page_name": "e-invoicing-sync", 13 | "roles": [], 14 | "script": null, 15 | "standard": "Yes", 16 | "style": null, 17 | "system_page": 1, 18 | "title": "EInvoicing Sync" 19 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/print_format/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/print_format/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/print_format/zatca_phase_1_print_format/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/print_format/zatca_phase_1_print_format/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/print_format/zatca_phase_1_print_format___pos_invoice/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/print_format/zatca_phase_1_print_format___pos_invoice/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/print_format/zatca_phase_2_print_format/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/print_format/zatca_phase_2_print_format/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/print_format/zatca_phase_2_print_format___pos_invoice/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/print_format/zatca_phase_2_print_format___pos_invoice/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/report/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/zatca_integration_details/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/report/zatca_integration_details/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/zatca_integration_details/zatca_integration_details.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, LavaLoon and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.query_reports["Zatca Integration Details"] = { 5 | "onload": function (report) { 6 | const summary_elm = document.getElementById('message-summary') 7 | if (!summary_elm) { 8 | const page_container = report.$page[0]; 9 | const filters_section = page_container.querySelector(".page-form"); 10 | const message = "This report will display the ZATCA status for each transaction and provide detailed invoice amount information, to reconcile transactions between the system and Fatoorah platform.\n" + 11 | "It is not recommended to run or generate this report with the “From Date” and “To Date” filters set for a period exceeding 31 days."; 12 | 13 | const message_summary_elm = document.createElement('div'); 14 | message_summary_elm.classList.add('my-3', 'mx-auto'); 15 | message_summary_elm.id = 'message-summary'; 16 | message_summary_elm.style.width = '95%'; 17 | 18 | const message_title = document.createElement('h5'); 19 | message_title.innerText = 'Summary'; 20 | const message_content = document.createElement('span'); 21 | message_content.innerText = message; 22 | 23 | message_summary_elm.append(document.createElement('hr'), message_title, message_content); 24 | filters_section.appendChild(message_summary_elm); 25 | } 26 | }, 27 | formatter:function (value, row, column, data, default_formatter) { 28 | value = default_formatter(value, row, column, data); 29 | if (column.fieldname == 'integration_status') { 30 | if (value.toLowerCase() =='accepted') { 31 | value = `${value}` 32 | } else if (value.toLowerCase() == 'rejected') { 33 | value = `${value}` 34 | } else if (value.toLowerCase() == 'resend') { 35 | value = `${value}` 36 | } else if (value.toLowerCase() == 'accepted with warnings') { 37 | value = `${value}` 38 | } 39 | } 40 | return value; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/zatca_integration_details/zatca_integration_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 1, 3 | "columns": [], 4 | "creation": "2024-08-28 15:58:47.083349", 5 | "disabled": 0, 6 | "docstatus": 0, 7 | "doctype": "Report", 8 | "filters": [ 9 | { 10 | "fieldname": "from_date_filter", 11 | "fieldtype": "Date", 12 | "label": "From Date", 13 | "mandatory": 1, 14 | "wildcard_filter": 0 15 | }, 16 | { 17 | "fieldname": "to_date_filter", 18 | "fieldtype": "Date", 19 | "label": "To Date", 20 | "mandatory": 1, 21 | "wildcard_filter": 0 22 | }, 23 | { 24 | "fieldname": "company_filter", 25 | "fieldtype": "Link", 26 | "label": "Company", 27 | "mandatory": 1, 28 | "options": "Company", 29 | "wildcard_filter": 0 30 | }, 31 | { 32 | "fieldname": "integration_status_filter", 33 | "fieldtype": "Select", 34 | "label": "Integration Status", 35 | "mandatory": 1, 36 | "options": "All\nReady For Batch\nResend\nAccepted with warnings\nAccepted\nRejected\nClearance switched off", 37 | "wildcard_filter": 0 38 | } 39 | ], 40 | "idx": 0, 41 | "is_standard": "Yes", 42 | "letterhead": null, 43 | "modified": "2024-08-29 12:38:36.976164", 44 | "modified_by": "Administrator", 45 | "module": "KSA Compliance", 46 | "name": "Zatca Integration Details", 47 | "owner": "Administrator", 48 | "prepared_report": 0, 49 | "ref_doctype": "Sales Invoice", 50 | "report_name": "Zatca Integration Details", 51 | "report_type": "Script Report", 52 | "roles": [ 53 | { 54 | "role": "Accounts Manager" 55 | }, 56 | { 57 | "role": "Sales Manager" 58 | }, 59 | { 60 | "role": "Accounts User" 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/zatca_integration_details/zatca_integration_details.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Lavaloon and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe import _ 6 | from datetime import datetime 7 | 8 | 9 | def execute(filters=None): 10 | columns, data, chart, report_summary = [], [], None, [] 11 | if not filters: 12 | return 13 | 14 | df = datetime.strptime(filters['from_date_filter'], '%Y-%m-%d') 15 | dt = datetime.strptime(filters['to_date_filter'], '%Y-%m-%d') 16 | if dt < df: 17 | frappe.throw( 18 | msg="""To date must be after From date. 19 | error_code='InvalidDateRange', 20 | code=400 21 | """ 22 | ) 23 | try: 24 | data = get_zatca_integration_details_data(filters=filters) 25 | columns = get_columns() 26 | records_count = len(data) 27 | report_summary = [ 28 | { 29 | 'value': records_count, 30 | 'label': 'Number of records', 31 | 'datatype': 'Number', 32 | }, 33 | ] 34 | 35 | values = {} 36 | labels = [] 37 | colors = [] 38 | for row in data: 39 | if row['integration_status'] not in labels: 40 | labels.append(row['integration_status']) 41 | if row['integration_status'] not in values: 42 | values[row['integration_status']] = 1 43 | else: 44 | values[row['integration_status']] += 1 45 | 46 | for label in labels: 47 | if label == 'Accepted': 48 | colors.append('green') 49 | elif label == 'Rejected': 50 | colors.append('red') 51 | elif label == 'Resend': 52 | colors.append('blue') 53 | elif label == 'Accepted with warnings': 54 | colors.append('yellow') 55 | chart = get_pie_chart_data( 56 | title='Zatca Integration Status', labels=labels, values=values, height=250, colors=colors 57 | ) 58 | 59 | # return columns, data, message, chart, report_summary, primitive_summary 60 | return columns, data, None, chart, report_summary 61 | 62 | except Exception as ex: 63 | frappe.throw(msg=f"""{str(ex)}""") 64 | 65 | 66 | def get_zatca_integration_details_data(filters): 67 | query = """ 68 | SELECT 69 | inv.name AS invoice_id, 70 | IFNULL(zi.integration_status,'N/A') integration_status, 71 | inv.posting_date, 72 | inv.net_total, 73 | inv.total_taxes_and_charges, 74 | inv.grand_total 75 | FROM 76 | `tabSales Invoice Additional Fields` zi 77 | RIGHT JOIN `tabSales Invoice` inv 78 | ON inv.name = zi.sales_invoice 79 | WHERE inv.company = %(company)s 80 | AND inv.posting_date BETWEEN %(from_date)s AND DATE_ADD(%(to_date)s, INTERVAL 1 DAY) 81 | AND zi.is_latest = 1 82 | AND ( 83 | (%(integration_status_filter)s = 'All' AND zi.integration_status IN ('All', 'Ready For Batch', 'Resend', 'Accepted with warnings', 'Accepted', 'Rejected', 'Clearance switched off')) 84 | ) OR 85 | ( 86 | (%(integration_status_filter)s != 'All' AND zi.integration_status = %(integration_status_filter)s) 87 | ) 88 | """ 89 | 90 | return frappe.db.sql( 91 | query=query, 92 | values={ 93 | 'from_date': filters['from_date_filter'], 94 | 'to_date': filters['to_date_filter'], 95 | 'company': filters['company_filter'], 96 | 'integration_status_filter': filters['integration_status_filter'], 97 | }, 98 | as_dict=1, 99 | ) 100 | 101 | 102 | def get_columns(): 103 | return [ 104 | { 105 | 'label': _('Invoice ID'), 106 | 'fieldname': 'invoice_id', 107 | 'fieldtype': 'Link', 108 | 'options': 'Sales Invoice', 109 | 'width': 200, 110 | }, 111 | { 112 | 'label': _('ZATCA Integration Status'), 113 | 'fieldname': 'integration_status', 114 | 'fieldtype': 'Data', 115 | 'width': 200, 116 | }, 117 | { 118 | 'label': _('Posting Date'), 119 | 'fieldname': 'posting_date', 120 | 'fieldtype': 'Date', 121 | 'width': 200, 122 | }, 123 | { 124 | 'label': _('Net Amount'), 125 | 'fieldname': 'net_total', 126 | 'fieldtype': 'Currency', 127 | 'width': 200, 128 | }, 129 | { 130 | 'label': _('VAT Amount'), 131 | 'fieldname': 'total_taxes_and_charges', 132 | 'fieldtype': 'Currency', 133 | 'width': 200, 134 | }, 135 | { 136 | 'label': _('Grand Total'), 137 | 'fieldname': 'grand_total', 138 | 'fieldtype': 'Currency', 139 | 'width': 200, 140 | }, 141 | ] 142 | 143 | 144 | def get_pie_chart_data(title, labels: [], values: [], height=250, colors=None): 145 | options = { 146 | 'title': title, 147 | 'data': {'labels': labels, 'datasets': [{'values': [values[label] for label in labels]}]}, 148 | 'type': 'pie', 149 | 'height': height, 150 | 'colors': colors, 151 | } 152 | 153 | return options 154 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/zatca_integration_summary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/ksa_compliance/report/zatca_integration_summary/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/zatca_integration_summary/zatca_integration_summary.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, LavaLoon and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.query_reports["Zatca Integration Summary"] = { 5 | "onload": function (report) { 6 | const summary_elm = document.getElementById('message-summary') 7 | if (!summary_elm) { 8 | const page_container = report.$page[0]; 9 | const filters_section = page_container.querySelector(".page-form"); 10 | const message = "A quick overview of ZATCA integration status totals and financial information is needed to monitor and reconcile transactions, ensuring all invoices are tracked and accounted for."; 11 | 12 | const message_summary_elm = document.createElement('div'); 13 | message_summary_elm.classList.add('my-3', 'mx-auto'); 14 | message_summary_elm.id = 'message-summary'; 15 | message_summary_elm.style.width = '95%'; 16 | 17 | const message_title = document.createElement('h5'); 18 | message_title.innerText = 'Summary'; 19 | const message_content = document.createElement('span'); 20 | message_content.innerText = message; 21 | 22 | message_summary_elm.append(document.createElement('hr'), message_title, message_content); 23 | filters_section.appendChild(message_summary_elm); 24 | } 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/zatca_integration_summary/zatca_integration_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "columns": [ 4 | { 5 | "fieldname": "integration_status", 6 | "fieldtype": "Data", 7 | "label": "ZATCA Integration status", 8 | "width": 200 9 | }, 10 | { 11 | "fieldname": "records_count", 12 | "fieldtype": "Int", 13 | "label": "Total Number of invoices", 14 | "width": 150 15 | }, 16 | { 17 | "fieldname": "net_total", 18 | "fieldtype": "Currency", 19 | "label": "Net Total Amount", 20 | "width": 150 21 | }, 22 | { 23 | "fieldname": "total_taxes_and_charges", 24 | "fieldtype": "Currency", 25 | "label": "VAT Total Amount", 26 | "width": 150 27 | }, 28 | { 29 | "fieldname": "grand_total", 30 | "fieldtype": "Currency", 31 | "label": "Grand Total Amount", 32 | "width": 150 33 | } 34 | ], 35 | "creation": "2024-08-29 11:13:47.816459", 36 | "disabled": 0, 37 | "docstatus": 0, 38 | "doctype": "Report", 39 | "filters": [ 40 | { 41 | "fieldname": "from_date_filter", 42 | "fieldtype": "Date", 43 | "label": "From Date", 44 | "mandatory": 1, 45 | "wildcard_filter": 0 46 | }, 47 | { 48 | "fieldname": "to_date_filter", 49 | "fieldtype": "Date", 50 | "label": "To Date", 51 | "mandatory": 1, 52 | "wildcard_filter": 0 53 | }, 54 | { 55 | "fieldname": "company_filter", 56 | "fieldtype": "Link", 57 | "label": "Company", 58 | "mandatory": 1, 59 | "options": "Company", 60 | "wildcard_filter": 0 61 | } 62 | ], 63 | "idx": 0, 64 | "is_standard": "Yes", 65 | "letterhead": null, 66 | "modified": "2024-08-29 13:32:30.000855", 67 | "modified_by": "Administrator", 68 | "module": "KSA Compliance", 69 | "name": "Zatca Integration Summary", 70 | "owner": "Administrator", 71 | "prepared_report": 0, 72 | "ref_doctype": "Sales Invoice", 73 | "report_name": "Zatca Integration Summary", 74 | "report_type": "Script Report", 75 | "roles": [ 76 | { 77 | "role": "Accounts Manager" 78 | }, 79 | { 80 | "role": "Accounts User" 81 | }, 82 | { 83 | "role": "Sales Manager" 84 | } 85 | ] 86 | } -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/report/zatca_integration_summary/zatca_integration_summary.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, LavaLoon and contributors 2 | # For license information, please see license.txt 3 | from ksa_compliance.ksa_compliance.report.zatca_integration_details.zatca_integration_details import get_pie_chart_data 4 | 5 | import frappe 6 | from datetime import datetime 7 | 8 | 9 | def execute(filters=None): 10 | data, chart, report_summary = [], None, [] 11 | 12 | if not filters: 13 | return 14 | 15 | df = datetime.strptime(filters['from_date_filter'], '%Y-%m-%d') 16 | dt = datetime.strptime(filters['to_date_filter'], '%Y-%m-%d') 17 | if dt < df: 18 | frappe.throw( 19 | msg=f"""Date range must be from {filters['from_date_filter']} to {filters['to_date_filter']}. 20 | error_code='InvalidDateRange', 21 | code=400 22 | """ 23 | ) 24 | 25 | data = get_zatca_integration_summary_data(filters=filters) 26 | 27 | if data: 28 | labels = [row['integration_status'] for row in data] 29 | values = {row['integration_status']: row['records_count'] for row in data} 30 | chart = get_pie_chart_data(title='Zatca Integration Status', labels=labels, values=values) 31 | records_count = sum(row['records_count'] for row in data) 32 | 33 | report_summary = [ 34 | { 35 | 'value': records_count, 36 | 'label': 'Number of records', 37 | 'datatype': 'Number', 38 | }, 39 | ] 40 | 41 | # return columns, data, message, chart, report_summary, primitive_summary 42 | return get_columns(), data, None, chart, report_summary 43 | 44 | 45 | def get_columns(): 46 | return [ 47 | {'fieldname': 'integration_status', 'fieldtype': 'Data', 'label': 'ZATCA Integration status', 'width': 200}, 48 | {'fieldname': 'records_count', 'fieldtype': 'Int', 'label': 'Total Number of invoices', 'width': 150}, 49 | {'fieldname': 'net_total', 'fieldtype': 'Currency', 'label': 'Net Total Amount', 'width': 150}, 50 | {'fieldname': 'total_taxes_and_charges', 'fieldtype': 'Currency', 'label': 'VAT Total Amount', 'width': 150}, 51 | {'fieldname': 'grand_total', 'fieldtype': 'Currency', 'label': 'Grand Total Amount', 'width': 150}, 52 | ] 53 | 54 | 55 | def get_zatca_integration_summary_data(filters): 56 | query = """ 57 | SELECT IFNULL(zi.integration_status,'N/A') AS integration_status, 58 | COUNT(DISTINCT inv.name) AS records_count, 59 | SUM(inv.net_total) AS net_total, 60 | SUM(inv.total_taxes_and_charges) AS total_taxes_and_charges, 61 | SUM(inv.grand_total) AS grand_total 62 | FROM 63 | `tabSales Invoice` inv 64 | LEFT JOIN `tabSales Invoice Additional Fields` zi 65 | ON inv.name = zi.sales_invoice 66 | WHERE inv.company = %(company)s 67 | AND inv.docstatus = 1 68 | AND inv.posting_date BETWEEN %(from_date)s AND DATE_ADD(%(to_date)s, INTERVAL 1 DAY) 69 | AND zi.is_latest = 1 70 | GROUP BY zi.integration_status 71 | """ 72 | 73 | return frappe.db.sql( 74 | query=query, 75 | values={ 76 | 'from_date': filters['from_date_filter'], 77 | 'to_date': filters['to_date_filter'], 78 | 'company': filters['company_filter'], 79 | }, 80 | as_dict=1, 81 | ) 82 | -------------------------------------------------------------------------------- /ksa_compliance/ksa_compliance/workspace/zatca/zatca.json: -------------------------------------------------------------------------------- 1 | { 2 | "charts": [ 3 | { 4 | "chart_name": "Invoice Integration Statistics", 5 | "label": "Invoice Integration Statistics" 6 | } 7 | ], 8 | "content": "[{\"id\":\"pG_Xe2iMd-\",\"type\":\"header\",\"data\":{\"text\":\"ZATCA Integration\",\"col\":12}},{\"id\":\"yD-yvwj2Lu\",\"type\":\"header\",\"data\":{\"text\":\"Insights\",\"col\":12}},{\"id\":\"58Jax4AthR\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Overall Integration Dashboard\",\"col\":4}},{\"id\":\"ubpMNkfpfI\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Invoice Integration Statistics\",\"col\":12}},{\"id\":\"cpqxKS7o2F\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Accepted Invoices\",\"col\":4}},{\"id\":\"G08r867GtY\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Accepted With Warnings invoices\",\"col\":4}},{\"id\":\"KCm42hMPJA\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Rejected Invoices\",\"col\":4}},{\"id\":\"S_Qm7oFLV0\",\"type\":\"header\",\"data\":{\"text\":\"Configuration & Transactions\",\"col\":12}},{\"id\":\"usnuiWNGFB\",\"type\":\"card\",\"data\":{\"card_name\":\"Business Configuration\",\"col\":4}},{\"id\":\"4hD6nr6Mev\",\"type\":\"card\",\"data\":{\"card_name\":\"Transactions\",\"col\":4}},{\"id\":\"AzDPNYsZQn\",\"type\":\"card\",\"data\":{\"card_name\":\"ZATCA Reports\",\"col\":4}},{\"id\":\"BWRXlzZMra\",\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}}]", 9 | "creation": "2024-05-29 15:20:46.009593", 10 | "custom_blocks": [], 11 | "docstatus": 0, 12 | "doctype": "Workspace", 13 | "for_user": "", 14 | "hide_custom": 0, 15 | "icon": "integration", 16 | "idx": 0, 17 | "indicator_color": "orange", 18 | "is_hidden": 0, 19 | "label": "ZATCA", 20 | "links": [ 21 | { 22 | "hidden": 0, 23 | "is_query_report": 0, 24 | "label": "Business Configuration", 25 | "link_count": 5, 26 | "link_type": "DocType", 27 | "onboard": 0, 28 | "type": "Card Break" 29 | }, 30 | { 31 | "hidden": 0, 32 | "is_query_report": 0, 33 | "label": "Company", 34 | "link_count": 0, 35 | "link_to": "Company", 36 | "link_type": "DocType", 37 | "onboard": 0, 38 | "type": "Link" 39 | }, 40 | { 41 | "hidden": 0, 42 | "is_query_report": 0, 43 | "label": "Address", 44 | "link_count": 0, 45 | "link_to": "Address", 46 | "link_type": "DocType", 47 | "onboard": 0, 48 | "type": "Link" 49 | }, 50 | { 51 | "hidden": 0, 52 | "is_query_report": 0, 53 | "label": "ZATCA Tax Category", 54 | "link_count": 0, 55 | "link_to": "Tax Category", 56 | "link_type": "DocType", 57 | "onboard": 0, 58 | "type": "Link" 59 | }, 60 | { 61 | "hidden": 0, 62 | "is_query_report": 0, 63 | "label": "ZATCA Phase 1 Business Settings", 64 | "link_count": 0, 65 | "link_to": "ZATCA Phase 1 Business Settings", 66 | "link_type": "DocType", 67 | "onboard": 0, 68 | "type": "Link" 69 | }, 70 | { 71 | "hidden": 0, 72 | "is_query_report": 0, 73 | "label": "ZATCA Phase 2 Business Settings", 74 | "link_count": 0, 75 | "link_to": "ZATCA Business Settings", 76 | "link_type": "DocType", 77 | "onboard": 0, 78 | "type": "Link" 79 | }, 80 | { 81 | "hidden": 0, 82 | "is_query_report": 0, 83 | "label": "ZATCA EGS", 84 | "link_count": 0, 85 | "link_to": "ZATCA EGS", 86 | "link_type": "DocType", 87 | "onboard": 0, 88 | "type": "Link" 89 | }, 90 | { 91 | "hidden": 0, 92 | "is_query_report": 0, 93 | "label": "ZATCA Invoice Counting Settings", 94 | "link_count": 0, 95 | "link_to": "ZATCA Invoice Counting Settings", 96 | "link_type": "DocType", 97 | "onboard": 0, 98 | "type": "Link" 99 | }, 100 | { 101 | "hidden": 0, 102 | "is_query_report": 0, 103 | "label": "Transactions", 104 | "link_count": 3, 105 | "link_type": "DocType", 106 | "onboard": 0, 107 | "type": "Card Break" 108 | }, 109 | { 110 | "hidden": 0, 111 | "is_query_report": 0, 112 | "label": "Customer", 113 | "link_count": 0, 114 | "link_to": "Customer", 115 | "link_type": "DocType", 116 | "onboard": 0, 117 | "type": "Link" 118 | }, 119 | { 120 | "hidden": 0, 121 | "is_query_report": 0, 122 | "label": "Sales Invoice", 123 | "link_count": 0, 124 | "link_to": "Sales Invoice", 125 | "link_type": "DocType", 126 | "onboard": 0, 127 | "type": "Link" 128 | }, 129 | { 130 | "hidden": 0, 131 | "is_query_report": 0, 132 | "label": "Sales Invoice Additional Fields", 133 | "link_count": 0, 134 | "link_to": "Sales Invoice Additional Fields", 135 | "link_type": "DocType", 136 | "onboard": 0, 137 | "type": "Link" 138 | }, 139 | { 140 | "hidden": 0, 141 | "is_query_report": 0, 142 | "label": "Logs", 143 | "link_count": 2, 144 | "link_type": "DocType", 145 | "onboard": 0, 146 | "type": "Card Break" 147 | }, 148 | { 149 | "hidden": 0, 150 | "is_query_report": 0, 151 | "label": "ZATCA Integration Log", 152 | "link_count": 0, 153 | "link_to": "ZATCA Integration Log", 154 | "link_type": "DocType", 155 | "onboard": 0, 156 | "type": "Link" 157 | }, 158 | { 159 | "hidden": 0, 160 | "is_query_report": 0, 161 | "label": "e-invoicing-sync", 162 | "link_count": 0, 163 | "link_to": "e-invoicing-sync", 164 | "link_type": "Page", 165 | "onboard": 0, 166 | "type": "Link" 167 | }, 168 | { 169 | "hidden": 0, 170 | "is_query_report": 0, 171 | "label": "ZATCA Reports", 172 | "link_count": 2, 173 | "link_type": "DocType", 174 | "onboard": 0, 175 | "type": "Card Break" 176 | }, 177 | { 178 | "hidden": 0, 179 | "is_query_report": 0, 180 | "label": "Zatca Integration Details", 181 | "link_count": 0, 182 | "link_to": "Zatca Integration Details", 183 | "link_type": "Report", 184 | "onboard": 0, 185 | "type": "Link" 186 | }, 187 | { 188 | "hidden": 0, 189 | "is_query_report": 0, 190 | "label": "Zatca Integration Summary", 191 | "link_count": 0, 192 | "link_to": "Zatca Integration Summary", 193 | "link_type": "Report", 194 | "onboard": 0, 195 | "type": "Link" 196 | } 197 | ], 198 | "modified": "2024-08-29 13:38:48.952561", 199 | "modified_by": "Administrator", 200 | "module": "KSA Compliance", 201 | "name": "ZATCA", 202 | "number_cards": [ 203 | { 204 | "label": "Accepted Invoices", 205 | "number_card_name": "Accepted Invoices" 206 | }, 207 | { 208 | "label": "Accepted With Warnings invoices", 209 | "number_card_name": "Accepted With Warnings invoices" 210 | }, 211 | { 212 | "label": "Rejected Invoices", 213 | "number_card_name": "Rejected Invoices" 214 | } 215 | ], 216 | "owner": "omar.ashraf@lavaloon.com", 217 | "parent_page": "", 218 | "public": 1, 219 | "quick_lists": [], 220 | "roles": [ 221 | { 222 | "role": "Accounts Manager" 223 | }, 224 | { 225 | "role": "Sales Manager" 226 | }, 227 | { 228 | "role": "System Manager" 229 | } 230 | ], 231 | "sequence_id": 1.0, 232 | "shortcuts": [ 233 | { 234 | "color": "Grey", 235 | "doc_view": "List", 236 | "label": "Overall Integration Dashboard", 237 | "link_to": "OverAll Integration", 238 | "type": "Dashboard" 239 | } 240 | ], 241 | "title": "ZATCA" 242 | } -------------------------------------------------------------------------------- /ksa_compliance/modules.txt: -------------------------------------------------------------------------------- 1 | KSA Compliance -------------------------------------------------------------------------------- /ksa_compliance/output_models/xsd/common/UBL-CommonSignatureComponents-2.1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ABIE 30 | UBL Document Signatures. Details 31 | This class collects all signature information for a document. 32 | UBL Document Signatures 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ASBIE 42 | UBL Document Signatures. Signature Information 43 | Each of these is scaffolding for a single digital signature. 44 | 1..n 45 | UBL Document Signatures 46 | Signature Information 47 | Signature Information 48 | Signature Information 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /ksa_compliance/output_models/xsd/common/UBL-CoreComponentParameters-2.1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 10 | 16 | 17 | 18 | 64 | -------------------------------------------------------------------------------- /ksa_compliance/output_models/xsd/common/UBL-ExtensionContentDataType-2.1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 10 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | Any element in any namespace other than the UBL extension 31 | namespace is allowed to be the apex element of an extension. 32 | Only those elements found in the UBL schemas and in the 33 | trees of schemas imported in this module are validated. 34 | Any element for which there is no schema declaration in any 35 | of the trees of schemas passes validation and is not 36 | treated as a schema constraint violation. 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ksa_compliance/output_models/xsd/common/UBL-QualifiedDataTypes-2.1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 70 | -------------------------------------------------------------------------------- /ksa_compliance/output_models/xsd/common/UBL-SignatureBasicComponents-2.1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 19 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ksa_compliance/output_models/xsd/common/UBL-XAdESv141-2.1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ksa_compliance/patches.txt: -------------------------------------------------------------------------------- 1 | [pre_model_sync] 2 | # Patches added in this section will be executed before doctypes are migrated 3 | # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations 4 | 5 | [post_model_sync] 6 | # Patches added in this section will be executed after doctypes are migrated 7 | ksa_compliance.patches._2024_02_27_add_counting_docs_for_existing_settings #2024-02-27 19:38 8 | ksa_compliance.patches._2024_03_20_update_blank_integration_status_in_additional_field #2024-03-21 10:24 9 | ksa_compliance.patches._2024_03_21_uuid_indexes 10 | ksa_compliance.patches._2024_03_21_update_last_attempt_in_additional_fields 11 | ksa_compliance.patches._2024_06_05_set_cli_setup_to_manual 12 | ksa_compliance.patches._2024_06_13_remove_custom_fields_from_sales_invoice #2024-06-13 11:25 13 | ksa_compliance.patches._2024_07_08_set_siaf_is_latest 14 | ksa_compliance.patches._2024_08_19_update_old_fatoora_url_in_business_settings #2024-08-19 11:41 15 | ksa_compliance.patches._2024_09_04_delete_obsolete_print_formats 16 | ksa_compliance.patches._2024_09_18_migrate_zatca_files_under_site -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_02_27_add_counting_docs_for_existing_settings.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | existing_settings = frappe.db.get_all('ZATCA Business Settings', ['name', 'company']) 6 | if existing_settings: 7 | for setting in existing_settings: 8 | max_invoice_counter = 0 9 | invoice_hash = 'NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==' 10 | 11 | max_invoice_counter = ( 12 | frappe.db.sql( 13 | """ 14 | SELECT MAX(ad.invoice_counter) AS max_invoice_counter 15 | FROM `tabSales Invoice Additional Fields` AS ad 16 | LEFT JOIN `tabSales Invoice` AS si 17 | ON ad.sales_invoice = si.name 18 | WHERE si.company = %(company)s 19 | """, 20 | {'company': setting.company}, 21 | as_dict=1, 22 | )[0].max_invoice_counter 23 | or 0 24 | ) 25 | 26 | if max_invoice_counter: 27 | invoice_hash = frappe.db.sql( 28 | """ 29 | SELECT ad.invoice_hash 30 | FROM `tabSales Invoice Additional Fields` AS ad 31 | LEFT JOIN `tabSales Invoice` AS si 32 | ON ad.sales_invoice = si.name 33 | WHERE si.company = %(company)s 34 | AND ad.invoice_counter = %(max_counter)s 35 | """, 36 | {'max_counter': max_invoice_counter, 'company': setting.company}, 37 | as_dict=1, 38 | )[0].invoice_hash 39 | 40 | if not frappe.db.exists('ZATCA Invoice Counting Settings', setting.name): 41 | invoice_counting_doc = frappe.new_doc('ZATCA Invoice Counting Settings') 42 | invoice_counting_doc.business_settings_reference = setting.name 43 | invoice_counting_doc.invoice_counter = max_invoice_counter 44 | invoice_counting_doc.previous_invoice_hash = invoice_hash 45 | invoice_counting_doc.insert(ignore_permissions=True) 46 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_03_20_update_blank_integration_status_in_additional_field.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | no_of_docs = len(frappe.get_all('Sales Invoice Additional Fields', {'integration_status': ''}, pluck='name')) 6 | print(f'Update {no_of_docs} Sales Invoice Additional Fields integration status field.') 7 | frappe.db.sql(""" 8 | UPDATE 9 | `tabSales Invoice Additional Fields` SET integration_status = 'Resend', docstatus = 0 10 | WHERE 11 | integration_status = '' 12 | """) 13 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_03_21_update_last_attempt_in_additional_fields.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | print('Updating last attempt with the last modified date.') 6 | frappe.db.sql(""" 7 | UPDATE `tabSales Invoice Additional Fields` 8 | SET last_attempt = modified 9 | WHERE last_attempt IS NULL 10 | """) 11 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_03_21_uuid_indexes.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | frappe.db.sql( 6 | 'CREATE UNIQUE INDEX IF NOT EXISTS lava_zatca_precomputed_invoice_uuid ON ' 7 | '`tabZATCA Precomputed Invoice` (invoice_uuid)' 8 | ) 9 | 10 | frappe.db.sql( 11 | 'CREATE UNIQUE INDEX IF NOT EXISTS lava_sales_invoice_additional_fields_uuid ON ' 12 | '`tabSales Invoice Additional Fields` (uuid)' 13 | ) 14 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_06_05_set_cli_setup_to_manual.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | frappe.db.sql("UPDATE `tabZATCA Business Settings` SET cli_setup = 'Manual'") 6 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_06_13_remove_custom_fields_from_sales_invoice.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | fields = [ 6 | 'Sales Invoice-custom_qr_code', 7 | 'Sales Invoice Item-custom_tax_total', 8 | 'Sales Invoice Item-custom_total_after_tax', 9 | 'Sales Invoice Item-custom_column_break_sm8qq', 10 | 'Sales Invoice Item-custom_section_break_ejvy9', 11 | ] 12 | for field in fields: 13 | print(f'Deleting custom field {field} if it exists') 14 | if frappe.db.exists('Custom Field', field): 15 | frappe.delete_doc('Custom Field', field) 16 | 17 | # Commit before schema changes 18 | frappe.db.commit() 19 | 20 | print('Removing QR code custom field from sales invoice') 21 | frappe.db.sql(""" 22 | ALTER TABLE `tabSales Invoice` 23 | DROP COLUMN IF EXISTS `custom_qr_code` 24 | """) 25 | 26 | print('Removing custom_tax_total and custom_total_after_tax from sales invoice items') 27 | frappe.db.sql(""" 28 | ALTER TABLE `tabSales Invoice Item` 29 | DROP COLUMN IF EXISTS `custom_tax_total`, 30 | DROP COLUMN IF EXISTS `custom_total_after_tax` 31 | """) 32 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_07_08_set_siaf_is_latest.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | frappe.db.sql( 6 | 'UPDATE `tabSales Invoice Additional Fields` siaf SET is_latest = 1 ' 7 | 'WHERE name = (SELECT name FROM `tabSales Invoice Additional Fields`' 8 | ' WHERE sales_invoice = siaf.sales_invoice ORDER BY modified DESC LIMIT 1)' 9 | ) 10 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_08_19_update_old_fatoora_url_in_business_settings.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | print('Start updating old fatoora url') 6 | sql = """ 7 | SELECT name, fatoora_server_url 8 | FROM `tabZATCA Business Settings` 9 | """ 10 | old_urls = frappe.db.sql(sql, as_dict=True) 11 | fatoora_servers = {'sandbox': 'Sandbox', 'simulation': 'Simulation', 'production': 'Production'} 12 | print('Updating Fatoora Server based on old urls') 13 | for url in old_urls: 14 | fatoora_server_url = url['fatoora_server_url'].strip() 15 | if fatoora_server_url.startswith('https://gw-fatoora.zatca.gov.sa/e-invoicing/developer-portal'): 16 | fatoora_server = fatoora_servers['sandbox'] 17 | elif fatoora_server_url.startswith('https://gw-fatoora.zatca.gov.sa/e-invoicing/simulation'): 18 | fatoora_server = fatoora_servers['simulation'] 19 | elif fatoora_server_url.startswith('https://gw-fatoora.zatca.gov.sa/e-invoicing/core'): 20 | fatoora_server = fatoora_servers['production'] 21 | else: 22 | fatoora_server = None 23 | if fatoora_server: 24 | print(f"Setting Fatoora Server for {url['name']} to {fatoora_server}") 25 | frappe.db.sql( 26 | """ 27 | UPDATE `tabZATCA Business Settings` SET fatoora_server = %(fatoora_server)s 28 | WHERE name = %(name)s 29 | """, 30 | {'fatoora_server': fatoora_server, 'name': url['name']}, 31 | ) 32 | print('Finish updating Fatoora server for all companies in business settings') 33 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_09_04_delete_obsolete_print_formats.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | names = [ 6 | 'ZATCA Simplified Sales Invoice', 7 | 'ZATCA Simplified Credit Invoice', 8 | 'ZATCA Simplified Debit Invoice', 9 | 'ZATCA Standard Sales Invoice', 10 | 'ZATCA Standard Credit Invoice', 11 | 'ZATCA Standard Debit Invoice', 12 | ] 13 | frappe.db.sql('DELETE FROM `tabPrint Format` WHERE name IN %(names)s', {'names': names}) 14 | -------------------------------------------------------------------------------- /ksa_compliance/patches/_2024_09_18_migrate_zatca_files_under_site.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from dataclasses import dataclass 4 | from typing import cast, List 5 | 6 | import frappe 7 | 8 | from ksa_compliance.ksa_compliance.doctype.zatca_business_settings.zatca_business_settings import ZATCABusinessSettings 9 | from ksa_compliance.zatca_files import ( 10 | get_zatca_tool_path, 11 | get_sandbox_private_key_path, 12 | get_csr_path, 13 | get_cert_path, 14 | get_compliance_cert_path, 15 | get_private_key_path, 16 | ) 17 | 18 | 19 | @dataclass 20 | class FileCopy: 21 | """ 22 | Copies a file from [src] to [dest], creating destination directories as needed 23 | """ 24 | 25 | src: str 26 | dest: str 27 | 28 | def describe(self) -> str: 29 | return f"Copying '{self.src}' to '{self.dest}'" 30 | 31 | # noinspection PyUnusedLocal 32 | def apply(self, verbose=False) -> None: 33 | print(self.describe()) 34 | if not os.path.isdir(os.path.dirname(self.dest)): 35 | os.makedirs(os.path.dirname(self.dest)) 36 | shutil.copy2(self.src, self.dest) 37 | 38 | 39 | @dataclass 40 | class DirectoryCopy: 41 | """ 42 | Copies a directory recursively from [src] to [dest], deleting [dest] first if it already exists. Use caution to 43 | avoid unintended directory deletion 44 | """ 45 | 46 | src: str 47 | dest: str 48 | 49 | def describe(self) -> str: 50 | if os.path.isdir(self.dest): 51 | return f"Deleting '{self.dest}', then copying '{self.src}' to '{self.dest}'" 52 | return f"Copying directory '{self.src}' to '{self.dest}'" 53 | 54 | def apply(self, verbose=False) -> None: 55 | def copy(src, dest): 56 | if verbose: 57 | print(f"Copying '{src}' to '{dest}'") 58 | shutil.copy2(src, dest, follow_symlinks=False) 59 | 60 | print(self.describe()) 61 | # If we do not delete the existing directory/files, we get permission errors (at least during local testing) 62 | # when trying to set permissions on already existing files. This deletion is still scary because it's a tree 63 | # deletion 64 | if os.path.isdir(self.dest): 65 | shutil.rmtree(self.dest) 66 | 67 | shutil.copytree(self.src, self.dest, dirs_exist_ok=True, copy_function=copy) 68 | 69 | 70 | class Migration: 71 | """ 72 | A migration describes a list of file/directory copy operations 73 | """ 74 | 75 | operations: List[FileCopy | DirectoryCopy] 76 | 77 | def __init__(self): 78 | self.operations = [] 79 | 80 | def add(self, operation: FileCopy | DirectoryCopy) -> None: 81 | self.operations.append(operation) 82 | 83 | def describe(self) -> str: 84 | if not self.operations: 85 | return 'No files to migrate' 86 | 87 | return '\n'.join([op.describe() for op in self.operations]) 88 | 89 | def apply(self, verbose=False) -> None: 90 | for op in self.operations: 91 | op.apply(verbose) 92 | 93 | 94 | def execute(dry_run=False, verbose=False): 95 | """ 96 | Migrates ZATCA files (certs, keys, csrs) and tools (CLI and JRE) from under sites and sites/zatca to 97 | sites/{site}/zatca-files and sites/{site}/zatca-tools 98 | """ 99 | if os.path.isdir('zatca'): 100 | migration = Migration() 101 | migration.add(DirectoryCopy('zatca', get_zatca_tool_path('.'))) 102 | if dry_run: 103 | print(migration.describe()) 104 | else: 105 | migration.apply(verbose) 106 | 107 | records = cast(list[dict], frappe.get_all('ZATCA Business Settings')) 108 | for record in records: 109 | settings = cast(ZATCABusinessSettings, frappe.get_doc('ZATCA Business Settings', record['name'])) 110 | print(f'Analyzing {settings.name}') 111 | migration = prepare_migration(settings) 112 | if dry_run: 113 | print(migration.describe()) 114 | else: 115 | migration.apply(verbose) 116 | # We need to update the CLI and JRE path to point to the ones inside the site 117 | # CLI/JRE paths are stored in absolute form. We first get the existing path relative to 'zatca' in the 118 | # 'sites' directory, so instead of '/home/frappe/frappe-bench/sites/zatca/{path} we get '{path}' 119 | # We then get that relative to the new tools directory, and convert to an absolute path again 120 | if settings.zatca_cli_path: 121 | new_cli_path = os.path.abspath(get_zatca_tool_path(os.path.relpath(settings.zatca_cli_path, 'zatca'))) 122 | if os.path.isfile(new_cli_path): 123 | print(f'Updating CLI path from {settings.zatca_cli_path} to {new_cli_path}') 124 | settings.zatca_cli_path = new_cli_path 125 | 126 | if settings.java_home: 127 | new_java_home = os.path.abspath(get_zatca_tool_path(os.path.relpath(settings.java_home, 'zatca'))) 128 | if os.path.isdir(new_java_home): 129 | print(f'Updating Java home from {settings.java_home} to {new_java_home}') 130 | settings.java_home = new_java_home 131 | 132 | settings.save() 133 | 134 | 135 | def prepare_migration(settings: ZATCABusinessSettings) -> Migration: 136 | migration = Migration() 137 | 138 | # We used to use the VAT as the prefix for file names (e.g. {vat}.pem for the certificate) which caused collisions 139 | # if the VAT is used by more than one company. We'll use the ID of the business settings going forward instead 140 | old_prefix = settings.vat_registration_number 141 | new_prefix = settings.file_prefix 142 | 143 | file_map = { 144 | old_prefix + '.csr': get_csr_path(new_prefix), 145 | old_prefix + '.privkey': get_private_key_path(new_prefix), 146 | old_prefix + '-compliance.pem': get_compliance_cert_path(new_prefix), 147 | old_prefix + '.pem': get_cert_path(new_prefix), 148 | 'sandbox_private_key.pem': get_sandbox_private_key_path(), 149 | } 150 | 151 | for src, dest in file_map.items(): 152 | if os.path.isfile(src): 153 | migration.add(FileCopy(src, dest)) 154 | 155 | return migration 156 | -------------------------------------------------------------------------------- /ksa_compliance/patches/zatca.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | from typing import cast 4 | 5 | import frappe 6 | from ksa_compliance.ksa_compliance.doctype.zatca_business_settings.zatca_business_settings import ZATCABusinessSettings 7 | 8 | 9 | def generate_compliance_cert_if_missing(): 10 | for id in frappe.get_all('ZATCA Business Settings'): 11 | settings = cast(ZATCABusinessSettings, frappe.get_doc('ZATCA Business Settings', id)) 12 | if bool(settings.security_token) and not os.path.isfile(settings.compliance_cert_path): 13 | print(f'Generating compliance certificate for {settings.name}') 14 | with open(settings.compliance_cert_path, 'wb+') as cert: 15 | cert.write(b'-----BEGIN CERTIFICATE-----\n') 16 | cert.write(base64.b64decode(settings.security_token)) 17 | cert.write(b'\n-----END CERTIFICATE-----') 18 | -------------------------------------------------------------------------------- /ksa_compliance/public/js/branch.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on("Branch", { 2 | setup: function (frm) { 3 | frm.set_df_property('custom_branch_ids', 'cannot_delete_rows', 1); 4 | frm.set_df_property('custom_branch_ids', 'cannot_add_rows', 1); 5 | }, 6 | refresh: async function (frm) { 7 | add_other_ids_if_new(frm) 8 | await filter_company_address(frm) 9 | }, 10 | custom_company: async function (frm) { 11 | await filter_company_address(frm) 12 | } 13 | }) 14 | 15 | async function fetch_company_address(frm) { 16 | if (frm.doc.custom_company) { 17 | const res = await frappe.call({ 18 | method: 19 | "ksa_compliance.ksa_compliance.doctype.zatca_business_settings.zatca_business_settings.fetch_company_addresses", 20 | args: { 21 | company_name: frm.doc.custom_company, 22 | } 23 | }); 24 | return res.message 25 | } 26 | return []; 27 | } 28 | 29 | async function filter_company_address(frm) { 30 | const addresses = await fetch_company_address(frm) 31 | frm.set_query("custom_company_address", function () { 32 | return { 33 | filters: { 34 | name: ["in", addresses], 35 | }, 36 | }; 37 | }); 38 | } 39 | 40 | function add_other_ids_if_new(frm) { 41 | if (frm.doc.custom_branch_ids.length === 0) { 42 | var seller_id_list = [ 43 | { 44 | type_name: "Commercial Registration Number", 45 | type_code: "CRN", 46 | } 47 | ]; 48 | frm.set_value("custom_branch_ids", seller_id_list); 49 | } 50 | } -------------------------------------------------------------------------------- /ksa_compliance/public/js/customer.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on("Customer", { 2 | setup: function(frm){ 3 | // Workaround for a change introduced in frappe v15.38.0: https://github.com/frappe/frappe/issues/27430 4 | if (frm.is_dialog) return; 5 | 6 | frm.set_df_property('custom_additional_ids', 'cannot_delete_rows', 1); 7 | frm.set_df_property('custom_additional_ids', 'cannot_add_rows', 1); 8 | }, 9 | refresh: function (frm) { 10 | add_other_ids_if_new(frm); 11 | }, 12 | }); 13 | 14 | function add_other_ids_if_new(frm) { 15 | // TODO: update permissions for child doctype 16 | if (frm.doc.custom_additional_ids.length === 0) { 17 | var buyer_id_list = []; 18 | buyer_id_list.push( 19 | { 20 | type_name: "Tax Identification Number", 21 | type_code: "TIN", 22 | }, 23 | { 24 | type_name: "Commercial Registration Number", 25 | type_code: "CRN", 26 | }, 27 | { 28 | type_name: "MOMRAH License", 29 | type_code: "MOM", 30 | }, 31 | { 32 | type_name: "MHRSD License", 33 | type_code: "MLS", 34 | }, 35 | { 36 | type_name: "700 Number", 37 | type_code: "700", 38 | }, 39 | { 40 | type_name: "MISA License", 41 | type_code: "SAG", 42 | }, 43 | { 44 | type_name: "National ID", 45 | type_code: "NAT", 46 | }, 47 | { 48 | type_name: "GCC ID", 49 | type_code: "GCC", 50 | }, 51 | { 52 | type_name: "Iqama", 53 | type_code: "IQA", 54 | }, 55 | { 56 | type_name: "Passport ID", 57 | type_code: "PAS", 58 | }, 59 | { 60 | type_name: "Other ID", 61 | type_code: "OTH", 62 | } 63 | ); 64 | frm.set_value("custom_additional_ids", buyer_id_list); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ksa_compliance/public/js/sales_invoice.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on('Sales Invoice', { 2 | setup: function (frm) { 3 | frm.set_query('custom_return_against_additional_references', function (doc) { 4 | // Similar to logic in erpnext/public/js/controllers/transaction.js for return_against 5 | let filters = { 6 | 'docstatus': 1, 7 | 'is_return': 0, 8 | 'company': doc.company 9 | }; 10 | if (frm.fields_dict['customer'] && doc.customer) filters['customer'] = doc.customer; 11 | if (frm.fields_dict['supplier'] && doc.supplier) filters['supplier'] = doc.supplier; 12 | 13 | return { 14 | filters: filters 15 | }; 16 | }); 17 | } 18 | }) -------------------------------------------------------------------------------- /ksa_compliance/standard_doctypes/branch.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from ksa_compliance.ksa_compliance.doctype.zatca_business_settings.zatca_business_settings import ZATCABusinessSettings 3 | from ksa_compliance.throw import fthrow 4 | from ksa_compliance.translation import ft 5 | 6 | 7 | def validate_branch(doc, method): 8 | validate_mandatory_crn(doc) 9 | validate_duplicate_crn(doc) 10 | 11 | 12 | def validate_mandatory_crn(doc): 13 | if ( 14 | doc.custom_branch_ids 15 | and doc.custom_company 16 | and ZATCABusinessSettings.is_branch_config_enabled(doc.custom_company) 17 | ): 18 | crn = doc.custom_branch_ids[0].value 19 | crn = crn.strip() or None if isinstance(crn, str) else crn 20 | if not crn: 21 | fthrow( 22 | msg=ft('CRN is mandatory when ZATCA branch configuration is enabled for company.'), 23 | title=ft('Mandatory CRN'), 24 | ) 25 | 26 | 27 | def validate_duplicate_crn(doc): 28 | if doc.custom_branch_ids: 29 | crn = doc.custom_branch_ids[0].value 30 | crn_exists = frappe.db.exists( 31 | 'Additional Seller IDs', 32 | {'parentfield': 'custom_branch_ids', 'parenttype': 'Branch', 'value': crn, 'parent': ['!=', doc.name]}, 33 | ) 34 | if crn and crn_exists: 35 | branch = frappe.get_value('Additional Seller IDs', crn_exists, 'parent') 36 | fthrow(msg=ft('This CRN is configured for branch: $branch', branch=branch), title=ft('Duplicate CRN Error')) 37 | -------------------------------------------------------------------------------- /ksa_compliance/standard_doctypes/sales_invoice.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import frappe 4 | import frappe.utils.background_jobs 5 | from erpnext.accounts.doctype.pos_invoice.pos_invoice import POSInvoice 6 | from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice 7 | from frappe import _ 8 | from frappe.utils import strip 9 | from result import is_ok 10 | 11 | from ksa_compliance import logger 12 | from ksa_compliance.ksa_compliance.doctype.sales_invoice_additional_fields.sales_invoice_additional_fields import ( 13 | SalesInvoiceAdditionalFields, 14 | ) 15 | from ksa_compliance.ksa_compliance.doctype.zatca_business_settings.zatca_business_settings import ZATCABusinessSettings 16 | from ksa_compliance.ksa_compliance.doctype.zatca_egs.zatca_egs import ZATCAEGS 17 | from ksa_compliance.ksa_compliance.doctype.zatca_phase_1_business_settings.zatca_phase_1_business_settings import ( 18 | ZATCAPhase1BusinessSettings, 19 | ) 20 | from ksa_compliance.ksa_compliance.doctype.zatca_precomputed_invoice.zatca_precomputed_invoice import ( 21 | ZATCAPrecomputedInvoice, 22 | ) 23 | from ksa_compliance.translation import ft 24 | 25 | IGNORED_INVOICES = set() 26 | 27 | 28 | def ignore_additional_fields_for_invoice(name: str) -> None: 29 | global IGNORED_INVOICES 30 | IGNORED_INVOICES.add(name) 31 | 32 | 33 | def clear_additional_fields_ignore_list() -> None: 34 | global IGNORED_INVOICES 35 | IGNORED_INVOICES.clear() 36 | 37 | 38 | def create_sales_invoice_additional_fields_doctype(self: SalesInvoice | POSInvoice, method): 39 | if self.doctype == 'Sales Invoice' and not _should_enable_zatca_for_invoice(self.name): 40 | logger.info(f"Skipping additional fields for {self.name} because it's before start date") 41 | return 42 | 43 | settings = ZATCABusinessSettings.for_invoice(self.name, self.doctype) 44 | if not settings: 45 | logger.info(f'Skipping additional fields for {self.name} because of missing ZATCA settings') 46 | return 47 | 48 | if not settings.enable_zatca_integration: 49 | logger.info(f'Skipping additional fields for {self.name} because ZATCA integration is disabled in settings') 50 | return 51 | 52 | global IGNORED_INVOICES 53 | if self.name in IGNORED_INVOICES: 54 | logger.info(f"Skipping additional fields for {self.name} because it's in the ignore list") 55 | return 56 | 57 | if self.doctype == 'Sales Invoice' and self.is_consolidated: 58 | logger.info(f"Skipping additional fields for {self.name} because it's consolidated") 59 | return 60 | 61 | si_additional_fields_doc = SalesInvoiceAdditionalFields.create_for_invoice(self.name, self.doctype) 62 | precomputed_invoice = ZATCAPrecomputedInvoice.for_invoice(self.name) 63 | is_live_sync = settings.is_live_sync 64 | if precomputed_invoice: 65 | logger.info(f'Using precomputed invoice {precomputed_invoice.name} for {self.name}') 66 | si_additional_fields_doc.use_precomputed_invoice(precomputed_invoice) 67 | 68 | egs_settings = ZATCAEGS.for_device(precomputed_invoice.device_id) 69 | if not egs_settings: 70 | logger.warning(f'Could not find EGS for device {precomputed_invoice.device_id}') 71 | else: 72 | # EGS Setting overrides company-wide setting 73 | is_live_sync = egs_settings.is_live_sync 74 | 75 | si_additional_fields_doc.insert() 76 | if is_live_sync: 77 | # We're running in the context of invoice submission (on_submit hook). We only want to run our ZATCA logic if 78 | # the invoice submits successfully after on_submit is run successfully from all apps. 79 | frappe.utils.background_jobs.enqueue( 80 | _submit_additional_fields, doc=si_additional_fields_doc, enqueue_after_commit=True 81 | ) 82 | 83 | 84 | def _submit_additional_fields(doc: SalesInvoiceAdditionalFields): 85 | logger.info(f'Submitting {doc.name}') 86 | result = doc.submit_to_zatca() 87 | message = result.ok_value if is_ok(result) else result.err_value 88 | logger.info(f'Submission result: {message}') 89 | 90 | 91 | def _should_enable_zatca_for_invoice(invoice_id: str) -> bool: 92 | start_date = date(2024, 3, 1) 93 | 94 | if frappe.db.table_exists('Vehicle Booking Item Info'): 95 | # noinspection SqlResolve 96 | records = frappe.db.sql( 97 | 'SELECT bv.local_trx_date_time FROM `tabVehicle Booking Item Info` bvii ' 98 | 'JOIN `tabBooking Vehicle` bv ON bvii.parent = bv.name WHERE bvii.sales_invoice = %(invoice)s', 99 | {'invoice': invoice_id}, 100 | as_dict=True, 101 | ) 102 | if records: 103 | local_date = records[0]['local_trx_date_time'].date() 104 | return local_date >= start_date 105 | 106 | posting_date = frappe.db.get_value('Sales Invoice', invoice_id, 'posting_date') 107 | return posting_date >= start_date 108 | 109 | 110 | def prevent_cancellation_of_sales_invoice(self: SalesInvoice | POSInvoice, method) -> None: 111 | is_phase_2_enabled_for_company = ZATCABusinessSettings.is_enabled_for_company(self.company) 112 | if is_phase_2_enabled_for_company: 113 | frappe.throw( 114 | msg=_('You cannot cancel sales invoice according to ZATCA Regulations.'), 115 | title=_('This Action Is Not Allowed'), 116 | ) 117 | 118 | 119 | def validate_sales_invoice(self: SalesInvoice | POSInvoice, method) -> None: 120 | valid = True 121 | is_phase_2_enabled_for_company = ZATCABusinessSettings.is_enabled_for_company(self.company) 122 | if ZATCAPhase1BusinessSettings.is_enabled_for_company(self.company) or is_phase_2_enabled_for_company: 123 | if len(self.taxes) == 0: 124 | frappe.msgprint( 125 | msg=_('Please include tax rate in Sales Taxes and Charges Table'), 126 | title=_('Validation Error'), 127 | indicator='red', 128 | ) 129 | valid = False 130 | 131 | if is_phase_2_enabled_for_company: 132 | settings = ZATCABusinessSettings.for_company(self.company) 133 | if settings.type_of_business_transactions == 'Standard Tax Invoices': 134 | customer = frappe.get_doc('Customer', self.customer) 135 | if not customer.custom_vat_registration_number and not any( 136 | [strip(x.value) for x in customer.custom_additional_ids] 137 | ): 138 | frappe.msgprint( 139 | ft( 140 | 'Company $company is configured to use Standard Tax Invoices, which require customers to ' 141 | 'define a VAT number or one of the other IDs. Please update customer $customer', 142 | company=self.company, 143 | customer=self.customer, 144 | ) 145 | ) 146 | valid = False 147 | 148 | if not valid: 149 | message_log = frappe.get_message_log() 150 | error_messages = '\n'.join(log['message'] for log in message_log) 151 | raise frappe.ValidationError(error_messages) 152 | -------------------------------------------------------------------------------- /ksa_compliance/standard_doctypes/sales_invoice_item.py: -------------------------------------------------------------------------------- 1 | # import frappe 2 | 3 | -------------------------------------------------------------------------------- /ksa_compliance/standard_doctypes/tax_category.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | import frappe 5 | 6 | 7 | @dataclass 8 | class ZatcaTaxCategory: 9 | """Holds ZATCA tax category code, reason and reason code""" 10 | 11 | tax_category_code: str = None 12 | reason_code: Optional[str] = None 13 | arabic_reason: Optional[str] = None 14 | 15 | 16 | def map_tax_category( 17 | tax_category_id: Optional[str] = None, item_tax_template_id: Optional[str] = None 18 | ) -> ZatcaTaxCategory: 19 | if tax_category_id: 20 | zatca_category, custom_category_reason = frappe.get_value( 21 | 'Tax Category', {'name': tax_category_id}, ['custom_zatca_category', 'custom_category_reason'] 22 | ) 23 | elif item_tax_template_id: 24 | zatca_category, custom_category_reason = frappe.get_value( 25 | 'Item Tax Template', 26 | {'name': item_tax_template_id}, 27 | ['custom_zatca_item_tax_category', 'custom_category_reason'], 28 | ) 29 | else: 30 | zatca_category = 'Standard rate' 31 | custom_category_reason = None 32 | 33 | zatca_category = zatca_category if zatca_category else 'Standard rate' 34 | if zatca_category == 'Standard rate': 35 | return ZatcaTaxCategory(_category_to_code(zatca_category)) 36 | 37 | category, reason = zatca_category.split(' || ') 38 | if custom_category_reason and reason == '{manual entry}': 39 | reason_data = _reason_to_code_and_arabic(reason, custom_category_reason) 40 | else: 41 | reason_data = _reason_to_code_and_arabic(reason) 42 | return ZatcaTaxCategory(_category_to_code(category), reason_data['reason_code'], reason_data['arabic_reason']) 43 | 44 | 45 | def _category_to_code(category: str) -> str: 46 | categories = { 47 | 'Standard rate': 'S', 48 | 'Exempt from Tax': 'E', 49 | 'Zero rated goods': 'Z', 50 | 'Services outside scope of tax / Not subject to VAT': 'O', 51 | } 52 | return categories[category] 53 | 54 | 55 | def _reason_to_code_and_arabic(reason: str, input_reason: Optional[str] = None) -> dict: 56 | # TODO: Update the lookup to use reason code instead of text decoded from the select field in tax category doctype. 57 | reasons = { 58 | 'Financial services mentioned in Article 29 of the VAT Regulations': { 59 | 'reason_code': 'VATEX-SA-29', 60 | 'arabic_reason': 'عقد تأمين على الحياة', 61 | }, 62 | 'Life insurance services mentioned in Article 29 of the VAT Regulations': { 63 | 'reason_code': 'VATEX-SA-29-7', 64 | 'arabic_reason': 'الخدمات المالية', 65 | }, 66 | 'Real estate transactions mentioned in Article 30 of the VAT Regulations': { 67 | 'reason_code': 'VATEX-SA-30', 68 | 'arabic_reason': 'التوريدات العقارية المعفاة من الضريبة', 69 | }, 70 | 'Export of goods': { 71 | 'reason_code': 'VATEX-SA-32', 72 | 'arabic_reason': 'صادرات السلع من المملكة', 73 | }, 74 | 'Export of services': { 75 | 'reason_code': 'VATEX-SA-33', 76 | 'arabic_reason': 'صادرات الخدمات من المملكة', 77 | }, 78 | 'The international transport of Goods': { 79 | 'reason_code': 'VATEX-SA-34-1', 80 | 'arabic_reason': 'النقل الدولي للسلع', 81 | }, 82 | 'International transport of passengers': { 83 | 'reason_code': 'VATEX-SA-34-2', 84 | 'arabic_reason': 'النقل الدولي للركاب', 85 | }, 86 | 'Services directly connected and incidental to a Supply of international passenger transport': { 87 | 'reason_code': 'VATEX-SA-34-3', 88 | 'arabic_reason': 'الخدمات المرتبطة مباشرة او عرضيًا بتوريد النقل الدولي للركاب', 89 | }, 90 | 'Supply of a qualifying means of transport': { 91 | 'reason_code': 'VATEX-SA-34-4', 92 | 'arabic_reason': 'توريد وسائل النقل المؤهلة', 93 | }, 94 | 'Any services relating to Goods or passenger transportation as defined in article twenty five of these ' 95 | 'Regulations': { 96 | 'reason_code': 'VATEX-SA-34-5', 97 | 'arabic_reason': 'الخدمات ذات الصلة بنقل السلع او الركاب، وفقاً للتعريف الوارد بالمادة الخامسة و العشرين ' 98 | 'من اللائحة التنفيذية لنظام ضريبة القيمة المضافة', 99 | }, 100 | 'Medicines and medical equipment': { 101 | 'reason_code': 'VATEX-SA-35', 102 | 'arabic_reason': 'الادوية والمعدات الطبية', 103 | }, 104 | 'Qualifying metals': { 105 | 'reason_code': 'VATEX-SA-36', 106 | 'arabic_reason': 'المعادن المؤهلة', 107 | }, 108 | 'Private education to citizen': { 109 | 'reason_code': 'VATEX-SA-EDU', 110 | 'arabic_reason': 'الخدمات التعليمية الخاصة للمواطنين', 111 | }, 112 | 'Private healthcare to citizen': { 113 | 'reason_code': 'VATEX-SA-HEA', 114 | 'arabic_reason': 'الخدمات الصحية الخاصة للمواطنين', 115 | }, 116 | 'Supply of qualified military goods': { 117 | 'reason_code': 'VATEX-SA-MLTRY', 118 | 'arabic_reason': 'توريد السلع العسكرية المؤهلة', 119 | }, 120 | '{manual entry}': {'reason_code': 'VATEX-SA-OOS', 'arabic_reason': input_reason}, 121 | 'Qualified Supply of Goods in Duty Free area': { 122 | 'reason_code': 'VATEX-SA-DUTYFREE', 123 | 'arabic_reason': 'التوريد المؤهل للسلع في الأسواق الحرة', 124 | }, 125 | } 126 | return reasons[reason] 127 | -------------------------------------------------------------------------------- /ksa_compliance/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/templates/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/templates/csr-config.properties: -------------------------------------------------------------------------------- 1 | csr.common.name={{ unit_common_name }} 2 | csr.serial.number={{ unit_serial_number }} 3 | csr.organization.identifier={{ vat_number }} 4 | csr.organization.unit.name={{ unit_name }} 5 | csr.organization.name={{ organization_name }} 6 | csr.country.name={{ country }} 7 | csr.invoice.type={{ invoice_type }} 8 | csr.location.address={{ address }} 9 | csr.industry.business.category={{ category }} -------------------------------------------------------------------------------- /ksa_compliance/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavaloon-eg/ksa_compliance/0764e184b3ff9abcf11baeb5d3f80bc3175da47a/ksa_compliance/templates/pages/__init__.py -------------------------------------------------------------------------------- /ksa_compliance/throw.py: -------------------------------------------------------------------------------- 1 | from typing import NoReturn 2 | 3 | import frappe 4 | from frappe import ValidationError 5 | 6 | 7 | def fthrow( 8 | msg: str, 9 | exc: type[Exception] = ValidationError, 10 | title: str | None = None, 11 | is_minimizable: bool = False, 12 | wide: bool = False, 13 | as_list: bool = False, 14 | ) -> NoReturn: 15 | """ 16 | A wrapper for frappe.throw that is annotated properly as having no return (i.e. throws an exception) 17 | 18 | frappe.throw is annotated as returning 'None' instead of 'NoReturn'. This messes up type analysis, especially with 19 | result type. For example, the following code: 20 | 21 | if is_err(result): 22 | frappe.throw("An error occurred") 23 | 24 | frappe.msgprint(result.ok_value) 25 | 26 | The msgprint would result in an analysis warning because the analyzer doesn't see that the error case ends 27 | at the throw. We'd have to use an explicit else, which causes unnecessary nesting. 28 | """ 29 | frappe.throw(msg, exc, title, is_minimizable, wide, as_list) 30 | -------------------------------------------------------------------------------- /ksa_compliance/translation.py: -------------------------------------------------------------------------------- 1 | from string import Template 2 | 3 | import frappe 4 | 5 | 6 | def ft(message: str, **kwargs) -> str: 7 | """ 8 | Translates a template string using frappe._. Use $ prefix for template variables in the message, and pass the 9 | values as keyword arguments 10 | 11 | Example: ft("Could not open file '$path'", path=filepath) 12 | """ 13 | # noinspection PyProtectedMember 14 | translated = frappe._(message) 15 | return Template(translated).substitute(kwargs) if kwargs else translated 16 | -------------------------------------------------------------------------------- /ksa_compliance/zatca_cli_setup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import subprocess 4 | import zipfile 5 | from logging import Logger 6 | from typing import Callable 7 | 8 | import frappe 9 | import requests 10 | from frappe.utils.logger import get_logger 11 | from requests import RequestException 12 | from requests.structures import CaseInsensitiveDict 13 | from result import Result, Err, Ok, is_err 14 | 15 | from ksa_compliance.translation import ft 16 | 17 | 18 | def download_with_progress(url: str, target_dir: str, progress: Callable[[float], None]) -> Result[str, str]: 19 | """ 20 | Downloads a file from [url] to [target_dir], reporting progress to [progress] (0.0 - 100.0) 21 | 22 | Returns the file name on success, an error message on failure. Request exceptions are automatically caught and 23 | reported to the 'Error Log' 24 | 25 | Only .gz and .zip files are allowed 26 | """ 27 | logger = _get_logger() 28 | try: 29 | with requests.get(url, stream=True) as response: 30 | file_name = _extract_filename_from_headers(response.headers) 31 | if is_err(file_name): 32 | return file_name 33 | 34 | extension = os.path.splitext(file_name.ok_value)[1] 35 | if not extension or extension not in ['.gz', '.zip']: 36 | return Err( 37 | ft('Only .zip and .gz files are supported. Extension deduced from server: $ext', ext=extension) 38 | ) 39 | 40 | current_size = 0 41 | total_size = int(response.headers.get('content-length', 0)) 42 | 43 | file_path = os.path.join(target_dir, file_name.ok_value) 44 | logger.info(f"Downloading '{file_path}' from '{url}' ({total_size / 1024} kib)") 45 | 46 | with open(file_path, 'wb') as f: 47 | for chunk in response.iter_content(chunk_size=1024 * 64): 48 | f.write(chunk) 49 | current_size += len(chunk) 50 | # noinspection PyBroadException 51 | try: 52 | progress(100 * float(current_size) / total_size) 53 | except Exception: 54 | pass 55 | 56 | return Ok(file_path) 57 | except RequestException as e: 58 | logger.error('Download failed', exc_info=True) 59 | frappe.log_error(title='ZATCA Setup Error') 60 | return Err(str(e)) 61 | 62 | 63 | def extract_archive(path: str) -> Result[str, str]: 64 | """ 65 | Extracts a tar.gz or zip archive into its containing directory. This expects the archives to contain a top-level 66 | directory 67 | """ 68 | base_dir = os.path.dirname(path) 69 | if path.endswith('.tar.gz'): 70 | # We get permissions error when overwriting existing files (previously extracted), so we recursively unlink first 71 | result = subprocess.run(['tar', 'zxvf', path, '-C', base_dir, '--recursive-unlink'], capture_output=True) 72 | if result.returncode != 0: 73 | return Err(ft("Failed to extract archive: '$path'", path=path)) 74 | 75 | home_dir = result.stdout.splitlines()[0].decode('utf-8') 76 | return Ok(os.path.join(base_dir, home_dir)) 77 | 78 | if path.endswith('.zip'): 79 | with zipfile.ZipFile(path, 'r') as archive: 80 | # We're assuming the archive contains a top-level directory 81 | home_dir = list(zipfile.Path(archive).iterdir())[0] 82 | archive.extractall(base_dir) 83 | return Ok(os.path.join(base_dir, home_dir.name)) 84 | 85 | return Err(ft('Unsupported archive format: $path', path=path)) 86 | 87 | 88 | def _extract_filename_from_headers(headers: CaseInsensitiveDict[str]) -> Result[str, str]: 89 | """ 90 | Extracts the filename from the response content disposition header and fails if it can't find and resolve a 91 | file name 92 | """ 93 | content_disposition = headers.get('content-disposition') 94 | if not content_disposition: 95 | return Err( 96 | ft("Can't figure out file name because the server response is missing the " "'Content-Disposition' header") 97 | ) 98 | 99 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#syntax 100 | # We're expecting something like: 101 | # Content-Disposition: attachment; filename="..." 102 | # The quotes are optional. They'll be stripped out if present 103 | parts = [p.strip() for p in content_disposition.split(';')] 104 | if len(parts) < 2 or parts[0] != 'attachment': 105 | return Err(ft("Expected an attachment 'Content-Disposition', got '$value' instead", value=parts[0])) 106 | 107 | if not parts[1].startswith('filename='): 108 | return Err(ft("'Content-Disposition' header doesn't specify a file name")) 109 | 110 | filename = parts[1][len('filename=') :].strip('"') 111 | if not filename: 112 | return Err(ft("'Content-Disposition' header doesn't specify a file name")) 113 | 114 | # Extract file name only and ignore any path-like parts (e.g. ../) 115 | return Ok(os.path.basename(filename)) 116 | 117 | 118 | def _get_logger() -> Logger: 119 | logger = get_logger('zatca-cli-setup') 120 | logger.setLevel(logging.INFO) 121 | return logger 122 | -------------------------------------------------------------------------------- /ksa_compliance/zatca_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import frappe 4 | 5 | 6 | def get_zatca_tool_path(relative_path: str = '.') -> str: 7 | """ 8 | Returns the path to use for a ZATCA tools (CLI or JRE). We want these files to 9 | persist across frappe cloud updates but exclude them from backup, so we use the site directory. Previously, we used 10 | 'sites', which was cleaned up after updates so users had to onboard after every update. 11 | """ 12 | return frappe.get_site_path(os.path.normpath(os.path.join('zatca-tools', relative_path))) 13 | 14 | 15 | def get_zatca_file_path(relative_path: str = '.') -> str: 16 | """ 17 | Returns the path to use for a ZATCA file (certificate, key, etc.). We want these files to 18 | persist across frappe cloud updates but exclude them from backup, so we use the site directory. Previously, we used 19 | 'sites', which was cleaned up after updates so users had to onboard after every update. 20 | """ 21 | return frappe.get_site_path(os.path.normpath(os.path.join('zatca-files', relative_path))) 22 | 23 | 24 | def get_sandbox_private_key_path(): 25 | return get_zatca_file_path('sandbox_private_key.pem') 26 | 27 | 28 | def get_csr_path(file_prefix: str) -> str: 29 | return get_zatca_file_path(f'{file_prefix}.csr') 30 | 31 | 32 | def get_cert_path(file_prefix: str) -> str: 33 | return get_zatca_file_path(f'{file_prefix}.pem') 34 | 35 | 36 | def get_compliance_cert_path(file_prefix: str) -> str: 37 | return get_zatca_file_path(f'{file_prefix}-compliance.pem') 38 | 39 | 40 | def get_private_key_path(file_prefix) -> str: 41 | return get_zatca_file_path(f'{file_prefix}.privkey') 42 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ksa_compliance" 3 | authors = [ 4 | { name = "LavaLoon", email = "info@lavaloon.com" } 5 | ] 6 | description = "KSA Compliance app for E-invoice" 7 | requires-python = ">=3.10" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | dependencies = [ 11 | # "frappe~=15.0.0" # Installed and managed by bench. 12 | "result", 13 | "pyqrcode~=1.2.1", 14 | "pathvalidate~=3.2.1", 15 | # frappe already requires a specific version of this, so we don't specify a version to avoid conflicts 16 | "semantic-version" 17 | ] 18 | 19 | [build-system] 20 | requires = ["flit_core >=3.4,<4"] 21 | build-backend = "flit_core.buildapi" 22 | 23 | # These dependencies are only installed when developer mode is enabled 24 | [tool.bench.dev-dependencies] 25 | # package_name = "~=1.1.0" 26 | ruff = "~=0.7.2" 27 | pre-commit = "~=4.0.1" 28 | mypy = "~=1.13.0" 29 | 30 | [tool.ruff] 31 | line-length = 120 32 | 33 | [tool.ruff.lint] 34 | typing-modules = ['frappe.types.DF'] 35 | 36 | [tool.ruff.format] 37 | quote-style = 'single' 38 | --------------------------------------------------------------------------------