mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 16:18:03 +00:00
Merge pull request #593 from SShadowS/al-support-clean
Add support for AL programming language
This commit is contained in:
18 files changed
+2837
-402
No files matched your search
@@ -2,6 +2,7 @@
|
||||
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
||||
|
||||
* Language support:
|
||||
* **Add support for AL (Application Language)** for Microsoft Dynamics 365 Business Central development. Requires VS Code AL extension (ms-dynamics-smb.al).
|
||||
* **Add support for R** via the R languageserver package with LSP integration, performance optimizations, and fallback symbol extraction
|
||||
* **Add support for Zig** via ZLS (cross-file references may not fully work on Windows)
|
||||
* **Add support for Lua** via lua-language-server
|
||||
|
||||
@@ -96,6 +96,7 @@ With Serena, we provide direct, out-of-the-box support for:
|
||||
* Nix (requires nixd installation)
|
||||
* Elixir (requires installation of NextLS and Elixir; **Windows not supported**)
|
||||
* Erlang (requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls), experimental, might be slow or hang)
|
||||
* AL
|
||||
|
||||
Support for further languages can easily be added by providing a shallow adapter for a new language server implementation,
|
||||
see Serena's [memory on that](.serena/memories/adding_new_language_support_guide.md).
|
||||
|
||||
@@ -63,6 +63,7 @@ dev = [
|
||||
"toml-sort>=0.24.2",
|
||||
"types-pyyaml>=6.0.12.20241230",
|
||||
"syrupy>=4.9.1",
|
||||
"types-requests>=2.32.4.20241230",
|
||||
]
|
||||
agno = ["agno>=1.2.6", "sqlalchemy>=2.0.40"]
|
||||
google = ["google-genai>=1.8.0"]
|
||||
@@ -271,6 +272,7 @@ markers = [
|
||||
"nix: language server running for Nix",
|
||||
"dart: language server running for Dart",
|
||||
"erlang: language server running for Erlang",
|
||||
"al: language server running for AL (Microsoft Dynamics 365 Business Central)",
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -112,3 +112,26 @@ class RuntimeDependencyCollection:
|
||||
FileUtils.download_and_extract_archive(logger, dep.url, dest, dep.archive_type)
|
||||
else:
|
||||
FileUtils.download_and_extract_archive(logger, dep.url, target_dir, dep.archive_type or "zip")
|
||||
|
||||
|
||||
def quote_windows_path(path: str) -> str:
|
||||
"""
|
||||
Quote a path for Windows command execution if needed.
|
||||
|
||||
On Windows, paths need to be quoted for proper command execution.
|
||||
The function checks if the path is already quoted to avoid double-quoting.
|
||||
On other platforms, the path is returned unchanged.
|
||||
|
||||
Args:
|
||||
path: The file path to potentially quote
|
||||
|
||||
Returns:
|
||||
The quoted path on Windows (if not already quoted), unchanged path on other platforms
|
||||
|
||||
"""
|
||||
if platform.system() == "Windows":
|
||||
# Check if already quoted to avoid double-quoting
|
||||
if path.startswith('"') and path.endswith('"'):
|
||||
return path
|
||||
return f'"{path}"'
|
||||
return path
|
||||
@@ -270,6 +270,11 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
ls = ErlangLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
|
||||
|
||||
elif config.code_language == Language.AL:
|
||||
from solidlsp.language_servers.al_language_server import ALLanguageServer
|
||||
|
||||
ls = ALLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
|
||||
|
||||
else:
|
||||
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
|
||||
raise SolidLSPException(f"Language {config.code_language} is not supported")
|
||||
|
||||
@@ -49,6 +49,7 @@ class Language(str, Enum):
|
||||
LUA = "lua"
|
||||
NIX = "nix"
|
||||
ERLANG = "erlang"
|
||||
AL = "al"
|
||||
# Experimental or deprecated Language Servers
|
||||
TYPESCRIPT_VTS = "typescript_vts"
|
||||
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
|
||||
@@ -130,6 +131,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.nix")
|
||||
case self.ERLANG:
|
||||
return FilenameMatcher("*.erl", "*.hrl", "*.escript", "*.config", "*.app", "*.app.src")
|
||||
case self.AL:
|
||||
return FilenameMatcher("*.al", "*.dal")
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "00000001-0000-0000-0000-000000000001",
|
||||
"name": "Test AL Project",
|
||||
"publisher": "Serena Test Publisher",
|
||||
"version": "1.0.0.0",
|
||||
"brief": "Test project for AL Language Server in Serena",
|
||||
"description": "This project contains AL code samples for testing language server features",
|
||||
"privacyStatement": "",
|
||||
"EULA": "",
|
||||
"help": "",
|
||||
"url": "https://github.com/oraios/serena",
|
||||
"logo": "",
|
||||
"dependencies": [],
|
||||
"screenshots": [],
|
||||
"platform": "1.0.0.0",
|
||||
"application": "26.0.0.0",
|
||||
"idRanges": [
|
||||
{
|
||||
"from": 50000,
|
||||
"to": 50100
|
||||
}
|
||||
],
|
||||
"resourceExposurePolicy": {
|
||||
"allowDebugging": true,
|
||||
"allowDownloadingSource": true,
|
||||
"includeSourceInSymbolFile": true
|
||||
},
|
||||
"runtime": "15.0",
|
||||
"features": ["NoImplicitWith"],
|
||||
"target": "Cloud"
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
codeunit 50000 CustomerMgt
|
||||
{
|
||||
Permissions = tabledata "TEST Customer" = rimd;
|
||||
|
||||
trigger OnRun()
|
||||
begin
|
||||
Message('Customer Management Codeunit');
|
||||
end;
|
||||
|
||||
procedure CreateNewCustomer()
|
||||
var
|
||||
Customer: Record "TEST Customer";
|
||||
CustomerCard: Page "TEST Customer Card";
|
||||
begin
|
||||
Customer.Init();
|
||||
Customer.Insert(true);
|
||||
|
||||
CustomerCard.SetRecord(Customer);
|
||||
CustomerCard.Run();
|
||||
end;
|
||||
|
||||
procedure CreateCustomer(CustomerNo: Code[20]; CustomerName: Text[100]; CustomerType: Enum CustomerType): Boolean
|
||||
var
|
||||
Customer: Record "TEST Customer";
|
||||
begin
|
||||
if Customer.Get(CustomerNo) then
|
||||
exit(false);
|
||||
|
||||
Customer.Init();
|
||||
Customer."No." := CustomerNo;
|
||||
Customer.Name := CustomerName;
|
||||
Customer."Customer Type" := CustomerType;
|
||||
Customer.UpdateSearchName();
|
||||
Customer.Insert(true);
|
||||
|
||||
exit(true);
|
||||
end;
|
||||
|
||||
procedure AssistEdit(var Customer: Record "TEST Customer"): Boolean
|
||||
var
|
||||
NoSeriesMgt: Codeunit NoSeriesManagement;
|
||||
begin
|
||||
with Customer do begin
|
||||
if NoSeriesMgt.SelectSeries(GetNoSeriesCode(), '', "No. Series") then begin
|
||||
NoSeriesMgt.SetSeries("No.");
|
||||
exit(true);
|
||||
end;
|
||||
end;
|
||||
exit(false);
|
||||
end;
|
||||
|
||||
procedure TestNoSeries()
|
||||
var
|
||||
SalesSetup: Record "Sales & Receivables Setup";
|
||||
begin
|
||||
SalesSetup.Get();
|
||||
SalesSetup.TestField("Customer Nos.");
|
||||
end;
|
||||
|
||||
procedure InitNo(var Customer: Record "TEST Customer")
|
||||
var
|
||||
NoSeriesMgt: Codeunit NoSeriesManagement;
|
||||
begin
|
||||
TestNoSeries();
|
||||
NoSeriesMgt.InitSeries(GetNoSeriesCode(), Customer."No. Series", 0D, Customer."No.", Customer."No. Series");
|
||||
end;
|
||||
|
||||
procedure GetNoSeriesCode(): Code[20]
|
||||
var
|
||||
SalesSetup: Record "Sales & Receivables Setup";
|
||||
begin
|
||||
SalesSetup.Get();
|
||||
exit(SalesSetup."Customer Nos.");
|
||||
end;
|
||||
|
||||
procedure CalculateTotalBalance(): Decimal
|
||||
var
|
||||
Customer: Record "TEST Customer";
|
||||
TotalBalance: Decimal;
|
||||
begin
|
||||
TotalBalance := 0;
|
||||
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
Customer.CalcFields(Balance);
|
||||
TotalBalance += Customer.Balance;
|
||||
until Customer.Next() = 0;
|
||||
|
||||
exit(TotalBalance);
|
||||
end;
|
||||
|
||||
procedure GetCustomerCount(CustomerType: Enum CustomerType): Integer
|
||||
var
|
||||
Customer: Record "TEST Customer";
|
||||
begin
|
||||
Customer.SetRange("Customer Type", CustomerType);
|
||||
exit(Customer.Count());
|
||||
end;
|
||||
|
||||
procedure BlockCustomersOverCreditLimit()
|
||||
var
|
||||
Customer: Record "TEST Customer";
|
||||
BlockedCount: Integer;
|
||||
begin
|
||||
BlockedCount := 0;
|
||||
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
Customer.CalcFields(Balance);
|
||||
if (Customer."Credit Limit" > 0) and (Customer.Balance > Customer."Credit Limit") then begin
|
||||
Customer.Blocked := true;
|
||||
Customer.Modify(true);
|
||||
BlockedCount += 1;
|
||||
end;
|
||||
until Customer.Next() = 0;
|
||||
|
||||
if BlockedCount > 0 then
|
||||
Message('%1 customers blocked due to credit limit exceeded', BlockedCount);
|
||||
end;
|
||||
|
||||
procedure GetPaymentProcessor(): Interface IPaymentProcessor
|
||||
var
|
||||
PaymentProcessorImpl: Codeunit PaymentProcessorImpl;
|
||||
begin
|
||||
exit(PaymentProcessorImpl);
|
||||
end;
|
||||
|
||||
procedure SendCustomerStatement(CustomerNo: Code[20])
|
||||
var
|
||||
Customer: Record "TEST Customer";
|
||||
ReportSelections: Record "Report Selections";
|
||||
begin
|
||||
if not Customer.Get(CustomerNo) then
|
||||
Error('Customer %1 not found', CustomerNo);
|
||||
|
||||
Customer.SetRecFilter();
|
||||
ReportSelections.SetRange(Usage, ReportSelections.Usage::"C.Statement");
|
||||
ReportSelections.PrintForCust(ReportSelections.Usage::"C.Statement", Customer, 1);
|
||||
end;
|
||||
|
||||
procedure ValidateEmail(Email: Text[80]): Boolean
|
||||
var
|
||||
MailMgt: Codeunit "Mail Management";
|
||||
begin
|
||||
exit(MailMgt.CheckValidEmailAddress(Email));
|
||||
end;
|
||||
|
||||
procedure MergeCustomers(FromCustomerNo: Code[20]; ToCustomerNo: Code[20])
|
||||
var
|
||||
FromCustomer: Record "TEST Customer";
|
||||
ToCustomer: Record "TEST Customer";
|
||||
CustomerLedgerEntry: Record "Cust. Ledger Entry";
|
||||
begin
|
||||
if not FromCustomer.Get(FromCustomerNo) then
|
||||
Error('Source customer %1 not found', FromCustomerNo);
|
||||
|
||||
if not ToCustomer.Get(ToCustomerNo) then
|
||||
Error('Target customer %1 not found', ToCustomerNo);
|
||||
|
||||
// Transfer ledger entries
|
||||
CustomerLedgerEntry.SetRange("Customer No.", FromCustomerNo);
|
||||
if CustomerLedgerEntry.FindSet() then
|
||||
repeat
|
||||
CustomerLedgerEntry."Customer No." := ToCustomerNo;
|
||||
CustomerLedgerEntry.Modify();
|
||||
until CustomerLedgerEntry.Next() = 0;
|
||||
|
||||
// Delete source customer
|
||||
FromCustomer.Delete(true);
|
||||
|
||||
Message('Customer %1 merged into %2', FromCustomerNo, ToCustomerNo);
|
||||
end;
|
||||
|
||||
[EventSubscriber(ObjectType::Table, Database::"TEST Customer", OnAfterInsertEvent, '', true, true)]
|
||||
local procedure OnAfterInsertCustomer(var Rec: Record "TEST Customer")
|
||||
begin
|
||||
LogCustomerChange(Rec, 'INSERT');
|
||||
end;
|
||||
|
||||
[EventSubscriber(ObjectType::Table, Database::"TEST Customer", OnAfterModifyEvent, '', true, true)]
|
||||
local procedure OnAfterModifyCustomer(var Rec: Record "TEST Customer")
|
||||
begin
|
||||
LogCustomerChange(Rec, 'MODIFY');
|
||||
end;
|
||||
|
||||
local procedure LogCustomerChange(Customer: Record "TEST Customer"; ChangeType: Text[10])
|
||||
var
|
||||
ChangeLogEntry: Record "Change Log Entry";
|
||||
begin
|
||||
// Log customer changes for audit purposes
|
||||
ChangeLogEntry.Init();
|
||||
ChangeLogEntry."Table No." := Database::"TEST Customer";
|
||||
ChangeLogEntry."Primary Key Field 1 Value" := Customer."No.";
|
||||
ChangeLogEntry."Type of Change" := ChangeLogEntry."Type of Change"::Modification;
|
||||
ChangeLogEntry."User ID" := UserId;
|
||||
ChangeLogEntry."Date and Time" := CurrentDateTime;
|
||||
if ChangeLogEntry.Insert() then;
|
||||
end;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
codeunit 50001 PaymentProcessorImpl implements IPaymentProcessor
|
||||
{
|
||||
procedure ProcessPayment(Customer: Record "TEST Customer"): Boolean
|
||||
var
|
||||
//PaymentEntry: Record "Payment Entry";
|
||||
begin
|
||||
// Implementation of payment processing
|
||||
Customer.CalcFields(Balance);
|
||||
|
||||
if Customer.Balance <= 0 then
|
||||
exit(false);
|
||||
|
||||
// PaymentEntry.Init();
|
||||
// PaymentEntry."Customer No." := Customer."No.";
|
||||
// PaymentEntry.Amount := Customer.Balance;
|
||||
// PaymentEntry."Payment Date" := Today();
|
||||
// PaymentEntry.Status := PaymentEntry.Status::Processed;
|
||||
|
||||
// if PaymentEntry.Insert(true) then begin
|
||||
// Message('Payment processed for customer %1', Customer.Name);
|
||||
// exit(true);
|
||||
// end;
|
||||
|
||||
exit(false);
|
||||
end;
|
||||
|
||||
procedure ValidatePaymentMethod(PaymentMethodCode: Code[10]): Boolean
|
||||
var
|
||||
PaymentMethod: Record "Payment Method";
|
||||
begin
|
||||
if PaymentMethodCode = '' then
|
||||
exit(false);
|
||||
|
||||
exit(PaymentMethod.Get(PaymentMethodCode));
|
||||
end;
|
||||
|
||||
procedure GetTransactionFee(Amount: Decimal): Decimal
|
||||
var
|
||||
FeePercentage: Decimal;
|
||||
MinimumFee: Decimal;
|
||||
begin
|
||||
FeePercentage := 2.9; // 2.9% transaction fee
|
||||
MinimumFee := 0.30; // Minimum fee
|
||||
|
||||
exit(Maximum(Amount * FeePercentage / 100, MinimumFee));
|
||||
end;
|
||||
|
||||
procedure RefundPayment(TransactionID: Text[50]): Boolean
|
||||
var
|
||||
// PaymentEntry: Record "Payment Entry";
|
||||
begin
|
||||
// PaymentEntry.SetRange("Transaction ID", TransactionID);
|
||||
|
||||
// if PaymentEntry.FindFirst() then begin
|
||||
// PaymentEntry.Status := PaymentEntry.Status::Refunded;
|
||||
// PaymentEntry."Refund Date" := Today();
|
||||
// PaymentEntry.Modify(true);
|
||||
|
||||
// Message('Payment refunded for transaction %1', TransactionID);
|
||||
// exit(true);
|
||||
// end;
|
||||
|
||||
Error('Transaction %1 not found', TransactionID);
|
||||
end;
|
||||
|
||||
local procedure Maximum(Value1: Decimal; Value2: Decimal): Decimal
|
||||
begin
|
||||
if Value1 > Value2 then
|
||||
exit(Value1)
|
||||
else
|
||||
exit(Value2);
|
||||
end;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
enum 50000 CustomerType
|
||||
{
|
||||
Extensible = true;
|
||||
Caption = 'Customer Type';
|
||||
|
||||
value(0; "")
|
||||
{
|
||||
Caption = '';
|
||||
}
|
||||
|
||||
value(1; Regular)
|
||||
{
|
||||
Caption = 'Regular';
|
||||
}
|
||||
|
||||
value(2; Premium)
|
||||
{
|
||||
Caption = 'Premium';
|
||||
}
|
||||
|
||||
value(3; VIP)
|
||||
{
|
||||
Caption = 'VIP';
|
||||
}
|
||||
|
||||
value(4; Corporate)
|
||||
{
|
||||
Caption = 'Corporate';
|
||||
}
|
||||
|
||||
value(5; Government)
|
||||
{
|
||||
Caption = 'Government';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
interface IPaymentProcessor
|
||||
{
|
||||
procedure ProcessPayment(Customer: Record "TEST Customer"): Boolean;
|
||||
procedure ValidatePaymentMethod(PaymentMethodCode: Code[10]): Boolean;
|
||||
procedure GetTransactionFee(Amount: Decimal): Decimal;
|
||||
procedure RefundPayment(TransactionID: Text[50]): Boolean;
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
page 50001 "TEST Customer Card"
|
||||
{
|
||||
Caption = 'Customer Card';
|
||||
PageType = Card;
|
||||
SourceTable = "TEST Customer";
|
||||
RefreshOnActivate = true;
|
||||
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
group(General)
|
||||
{
|
||||
Caption = 'General';
|
||||
|
||||
field("No."; Rec."No.")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the customer number.';
|
||||
|
||||
trigger OnAssistEdit()
|
||||
begin
|
||||
if CustomerMgt.AssistEdit(Rec) then
|
||||
CurrPage.Update();
|
||||
end;
|
||||
}
|
||||
|
||||
field(Name; Rec.Name)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the customer name.';
|
||||
ShowMandatory = true;
|
||||
}
|
||||
|
||||
field("Search Name"; Rec."Search Name")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the search name.';
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
field("Customer Type"; Rec."Customer Type")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the type of customer.';
|
||||
}
|
||||
|
||||
field(Blocked; Rec.Blocked)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies if the customer is blocked.';
|
||||
}
|
||||
|
||||
field("Last Date Modified"; Rec."Last Date Modified")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies when the customer was last modified.';
|
||||
Editable = false;
|
||||
}
|
||||
}
|
||||
|
||||
group(AddressAndContact)
|
||||
{
|
||||
Caption = 'Address & Contact';
|
||||
|
||||
field(Address; Rec.Address)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the customer address.';
|
||||
}
|
||||
|
||||
field("Address 2"; Rec."Address 2")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies additional address information.';
|
||||
}
|
||||
|
||||
field(City; Rec.City)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the city.';
|
||||
}
|
||||
|
||||
field("Phone No."; Rec."Phone No.")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the phone number.';
|
||||
}
|
||||
|
||||
field("E-Mail"; Rec."E-Mail")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the email address.';
|
||||
ExtendedDatatype = EMail;
|
||||
}
|
||||
}
|
||||
|
||||
group(Invoicing)
|
||||
{
|
||||
Caption = 'Invoicing';
|
||||
|
||||
field("Credit Limit"; Rec."Credit Limit")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the credit limit.';
|
||||
}
|
||||
|
||||
field(Balance; Rec.Balance)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the customer balance.';
|
||||
DrillDownPageId = "Customer Ledger Entries";
|
||||
}
|
||||
|
||||
field("Payment Terms Code"; Rec."Payment Terms Code")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the payment terms.';
|
||||
}
|
||||
|
||||
field("Currency Code"; Rec."Currency Code")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the currency code.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
area(FactBoxes)
|
||||
{
|
||||
part(CustomerPicture; "Customer Picture")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
SubPageLink = "No." = field("No.");
|
||||
}
|
||||
|
||||
systempart(Links; Links)
|
||||
{
|
||||
ApplicationArea = RecordLinks;
|
||||
}
|
||||
|
||||
systempart(Notes; Notes)
|
||||
{
|
||||
ApplicationArea = Notes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actions
|
||||
{
|
||||
area(Navigation)
|
||||
{
|
||||
group(Customer)
|
||||
{
|
||||
Caption = '&Customer';
|
||||
|
||||
action(LedgerEntries)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Ledger E&ntries';
|
||||
Image = CustomerLedger;
|
||||
RunObject = page "Customer Ledger Entries";
|
||||
RunPageLink = "Customer No." = field("No.");
|
||||
RunPageView = sorting("Customer No.");
|
||||
ShortcutKey = 'Ctrl+F7';
|
||||
ToolTip = 'View the history of transactions for the customer.';
|
||||
}
|
||||
|
||||
action(Statistics)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Statistics';
|
||||
Image = Statistics;
|
||||
RunObject = page "Customer Statistics";
|
||||
RunPageLink = "No." = field("No.");
|
||||
ShortcutKey = 'F7';
|
||||
ToolTip = 'View statistical information about the customer.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
area(Processing)
|
||||
{
|
||||
group(Functions)
|
||||
{
|
||||
Caption = 'F&unctions';
|
||||
|
||||
action(CheckCreditLimit)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Check Credit Limit';
|
||||
Image = Check;
|
||||
ToolTip = 'Check if the customer has exceeded their credit limit.';
|
||||
|
||||
trigger OnAction()
|
||||
begin
|
||||
Rec.CheckCreditLimit();
|
||||
end;
|
||||
}
|
||||
|
||||
action(ProcessPayment)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Process Payment';
|
||||
Image = Payment;
|
||||
ToolTip = 'Process a payment for this customer.';
|
||||
|
||||
trigger OnAction()
|
||||
var
|
||||
PaymentProcessor: Interface IPaymentProcessor;
|
||||
begin
|
||||
PaymentProcessor := CustomerMgt.GetPaymentProcessor();
|
||||
PaymentProcessor.ProcessPayment(Rec);
|
||||
end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
area(Promoted)
|
||||
{
|
||||
group(Category_Process)
|
||||
{
|
||||
Caption = 'Process';
|
||||
|
||||
actionref(CheckCreditLimit_Promoted; CheckCreditLimit)
|
||||
{
|
||||
}
|
||||
|
||||
actionref(ProcessPayment_Promoted; ProcessPayment)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
group(Category_Customer)
|
||||
{
|
||||
Caption = 'Customer';
|
||||
|
||||
actionref(Statistics_Promoted; Statistics)
|
||||
{
|
||||
}
|
||||
|
||||
actionref(LedgerEntries_Promoted; LedgerEntries)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var
|
||||
CustomerMgt: Codeunit CustomerMgt;
|
||||
|
||||
trigger OnOpenPage()
|
||||
begin
|
||||
Rec.SetRange("Customer Type");
|
||||
end;
|
||||
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
CheckCreditStatus();
|
||||
end;
|
||||
|
||||
local procedure CheckCreditStatus()
|
||||
begin
|
||||
if Rec."Credit Limit" = 0 then
|
||||
exit;
|
||||
|
||||
Rec.CalcFields(Balance);
|
||||
if Rec.Balance > Rec."Credit Limit" then
|
||||
Message('Warning: Customer has exceeded credit limit');
|
||||
end;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
page 50002 "TEST Customer List"
|
||||
{
|
||||
Caption = 'Customer List';
|
||||
PageType = List;
|
||||
ApplicationArea = All;
|
||||
UsageCategory = Lists;
|
||||
SourceTable = "TEST Customer";
|
||||
CardPageId = "TEST Customer Card";
|
||||
Editable = false;
|
||||
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
repeater(Group)
|
||||
{
|
||||
field("No."; Rec."No.")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the customer number.';
|
||||
}
|
||||
|
||||
field(Name; Rec.Name)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the customer name.';
|
||||
}
|
||||
|
||||
field(City; Rec.City)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the city.';
|
||||
}
|
||||
|
||||
field("Customer Type"; Rec."Customer Type")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the type of customer.';
|
||||
}
|
||||
|
||||
field("Phone No."; Rec."Phone No.")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the phone number.';
|
||||
}
|
||||
|
||||
field("E-Mail"; Rec."E-Mail")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the email address.';
|
||||
}
|
||||
|
||||
field(Balance; Rec.Balance)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the customer balance.';
|
||||
StyleExpr = BalanceStyleExpr;
|
||||
}
|
||||
|
||||
field("Credit Limit"; Rec."Credit Limit")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the credit limit.';
|
||||
}
|
||||
|
||||
field(Blocked; Rec.Blocked)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies if the customer is blocked.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
area(FactBoxes)
|
||||
{
|
||||
systempart(Links; Links)
|
||||
{
|
||||
ApplicationArea = RecordLinks;
|
||||
}
|
||||
|
||||
systempart(Notes; Notes)
|
||||
{
|
||||
ApplicationArea = Notes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actions
|
||||
{
|
||||
area(Processing)
|
||||
{
|
||||
action(NewCustomer)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'New';
|
||||
Image = NewCustomer;
|
||||
ToolTip = 'Create a new customer.';
|
||||
|
||||
trigger OnAction()
|
||||
begin
|
||||
CustomerMgt.CreateNewCustomer();
|
||||
end;
|
||||
}
|
||||
|
||||
action(ExportToExcel)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Export to Excel';
|
||||
Image = ExportToExcel;
|
||||
ToolTip = 'Export the customer list to Excel.';
|
||||
|
||||
trigger OnAction()
|
||||
begin
|
||||
ExportCustomersToExcel();
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
area(Navigation)
|
||||
{
|
||||
action(ViewStatistics)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Statistics';
|
||||
Image = Statistics;
|
||||
RunObject = page "Customer Statistics";
|
||||
RunPageLink = "No." = field("No.");
|
||||
ToolTip = 'View customer statistics.';
|
||||
}
|
||||
}
|
||||
|
||||
area(Promoted)
|
||||
{
|
||||
group(Category_New)
|
||||
{
|
||||
Caption = 'New';
|
||||
|
||||
actionref(NewCustomer_Promoted; NewCustomer)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
group(Category_Process)
|
||||
{
|
||||
Caption = 'Process';
|
||||
|
||||
actionref(ExportToExcel_Promoted; ExportToExcel)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
SetBalanceStyle();
|
||||
end;
|
||||
|
||||
var
|
||||
CustomerMgt: Codeunit CustomerMgt;
|
||||
BalanceStyleExpr: Text;
|
||||
|
||||
local procedure SetBalanceStyle()
|
||||
begin
|
||||
BalanceStyleExpr := '';
|
||||
|
||||
Rec.CalcFields(Balance);
|
||||
if (Rec."Credit Limit" <> 0) and (Rec.Balance > Rec."Credit Limit") then
|
||||
BalanceStyleExpr := 'Unfavorable';
|
||||
end;
|
||||
|
||||
local procedure ExportCustomersToExcel()
|
||||
var
|
||||
ExcelBuffer: Record "Excel Buffer" temporary;
|
||||
RowNo: Integer;
|
||||
begin
|
||||
ExcelBuffer.Reset();
|
||||
ExcelBuffer.DeleteAll();
|
||||
|
||||
// Add headers
|
||||
RowNo := 1;
|
||||
ExcelBuffer.AddColumn('Customer No.', false, '', false, false, false, '', ExcelBuffer."Cell Type"::Text);
|
||||
ExcelBuffer.AddColumn('Name', false, '', false, false, false, '', ExcelBuffer."Cell Type"::Text);
|
||||
ExcelBuffer.AddColumn('City', false, '', false, false, false, '', ExcelBuffer."Cell Type"::Text);
|
||||
ExcelBuffer.AddColumn('Balance', false, '', false, false, false, '', ExcelBuffer."Cell Type"::Number);
|
||||
ExcelBuffer.NewRow();
|
||||
|
||||
// Add data
|
||||
if rec.FindSet() then
|
||||
repeat
|
||||
RowNo += 1;
|
||||
rec.CalcFields(Balance);
|
||||
ExcelBuffer.AddColumn(rec."No.", false, '', false, false, false, '', ExcelBuffer."Cell Type"::Text);
|
||||
ExcelBuffer.AddColumn(rec.Name, false, '', false, false, false, '', ExcelBuffer."Cell Type"::Text);
|
||||
ExcelBuffer.AddColumn(rec.City, false, '', false, false, false, '', ExcelBuffer."Cell Type"::Text);
|
||||
ExcelBuffer.AddColumn(rec.Balance, false, '', false, false, false, '', ExcelBuffer."Cell Type"::Number);
|
||||
ExcelBuffer.NewRow();
|
||||
until rec.Next() = 0;
|
||||
|
||||
ExcelBuffer.CreateNewBook('Customers');
|
||||
ExcelBuffer.WriteSheet('Customer List', CompanyName, UserId);
|
||||
ExcelBuffer.CloseBook();
|
||||
ExcelBuffer.OpenExcel();
|
||||
end;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
tableextension 50000 ItemExt extends Item
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(50000; "Customer No."; Code[20])
|
||||
{
|
||||
Caption = 'Preferred Customer No.';
|
||||
DataClassification = CustomerContent;
|
||||
TableRelation = "TEST Customer";
|
||||
|
||||
trigger OnValidate()
|
||||
var
|
||||
Customer: Record "TEST Customer";
|
||||
begin
|
||||
if "Customer No." <> '' then begin
|
||||
Customer.Get("Customer No.");
|
||||
"Customer Name" := Customer.Name;
|
||||
end else
|
||||
"Customer Name" := '';
|
||||
end;
|
||||
}
|
||||
|
||||
field(50001; "Customer Name"; Text[100])
|
||||
{
|
||||
Caption = 'Preferred Customer Name';
|
||||
DataClassification = CustomerContent;
|
||||
Editable = false;
|
||||
}
|
||||
|
||||
field(50002; "Special Discount %"; Decimal)
|
||||
{
|
||||
Caption = 'Special Discount %';
|
||||
DataClassification = CustomerContent;
|
||||
MinValue = 0;
|
||||
MaxValue = 100;
|
||||
}
|
||||
|
||||
field(50003; "Last Sale Date"; Date)
|
||||
{
|
||||
Caption = 'Last Sale Date';
|
||||
DataClassification = CustomerContent;
|
||||
Editable = false;
|
||||
}
|
||||
|
||||
field(50004; "Total Sales Qty"; Decimal)
|
||||
{
|
||||
Caption = 'Total Sales Quantity';
|
||||
FieldClass = FlowField;
|
||||
CalcFormula = sum("Sales Line".Quantity where("No." = field("No."),
|
||||
Type = const(Item)));
|
||||
Editable = false;
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(CustomerKey; "Customer No.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
procedure UpdateLastSaleDate()
|
||||
begin
|
||||
"Last Sale Date" := Today();
|
||||
Modify();
|
||||
end;
|
||||
|
||||
procedure GetSpecialPrice(Customer: Record "TEST Customer"): Decimal
|
||||
var
|
||||
BasePrice: Decimal;
|
||||
begin
|
||||
BasePrice := "Unit Price";
|
||||
|
||||
if "Customer No." = Customer."No." then
|
||||
BasePrice := BasePrice * (1 - "Special Discount %" / 100);
|
||||
|
||||
exit(BasePrice);
|
||||
end;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
table 50000 "TEST Customer"
|
||||
{
|
||||
Caption = 'Customer';
|
||||
DataClassification = CustomerContent;
|
||||
|
||||
fields
|
||||
{
|
||||
field(1; "No."; Code[20])
|
||||
{
|
||||
Caption = 'No.';
|
||||
DataClassification = CustomerContent;
|
||||
|
||||
trigger OnValidate()
|
||||
begin
|
||||
if "No." <> xRec."No." then begin
|
||||
CustomerMgt.TestNoSeries();
|
||||
"No. Series" := '';
|
||||
end;
|
||||
end;
|
||||
}
|
||||
|
||||
field(2; Name; Text[100])
|
||||
{
|
||||
Caption = 'Name';
|
||||
DataClassification = CustomerContent;
|
||||
|
||||
trigger OnValidate()
|
||||
begin
|
||||
if Name <> xRec.Name then
|
||||
UpdateSearchName();
|
||||
end;
|
||||
}
|
||||
|
||||
field(3; "Search Name"; Code[100])
|
||||
{
|
||||
Caption = 'Search Name';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(4; Address; Text[100])
|
||||
{
|
||||
Caption = 'Address';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(5; "Address 2"; Text[50])
|
||||
{
|
||||
Caption = 'Address 2';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(6; City; Text[30])
|
||||
{
|
||||
Caption = 'City';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(7; "Phone No."; Text[30])
|
||||
{
|
||||
Caption = 'Phone No.';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(8; "E-Mail"; Text[80])
|
||||
{
|
||||
Caption = 'E-Mail';
|
||||
DataClassification = CustomerContent;
|
||||
|
||||
trigger OnValidate()
|
||||
var
|
||||
MailMgt: Codeunit "Mail Management";
|
||||
begin
|
||||
MailMgt.CheckValidEmailAddresses("E-Mail");
|
||||
end;
|
||||
}
|
||||
|
||||
field(10; "Customer Type"; Enum CustomerType)
|
||||
{
|
||||
Caption = 'Customer Type';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(11; Balance; Decimal)
|
||||
{
|
||||
Caption = 'Balance';
|
||||
Editable = false;
|
||||
FieldClass = FlowField;
|
||||
CalcFormula = sum("Cust. Ledger Entry".Amount where("Customer No." = field("No.")));
|
||||
}
|
||||
|
||||
field(12; "Credit Limit"; Decimal)
|
||||
{
|
||||
Caption = 'Credit Limit';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(13; Blocked; Boolean)
|
||||
{
|
||||
Caption = 'Blocked';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(14; "Last Date Modified"; Date)
|
||||
{
|
||||
Caption = 'Last Date Modified';
|
||||
DataClassification = CustomerContent;
|
||||
Editable = false;
|
||||
}
|
||||
|
||||
field(15; "No. Series"; Code[20])
|
||||
{
|
||||
Caption = 'No. Series';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
|
||||
field(20; "Payment Terms Code"; Code[10])
|
||||
{
|
||||
Caption = 'Payment Terms Code';
|
||||
DataClassification = CustomerContent;
|
||||
TableRelation = "Payment Terms";
|
||||
}
|
||||
|
||||
field(21; "Currency Code"; Code[10])
|
||||
{
|
||||
Caption = 'Currency Code';
|
||||
DataClassification = CustomerContent;
|
||||
TableRelation = Currency;
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(PK; "No.")
|
||||
{
|
||||
Clustered = true;
|
||||
}
|
||||
|
||||
key(SearchName; "Search Name")
|
||||
{
|
||||
}
|
||||
|
||||
key(CustomerType; "Customer Type", City)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
fieldgroups
|
||||
{
|
||||
fieldgroup(DropDown; "No.", Name, City)
|
||||
{
|
||||
}
|
||||
|
||||
fieldgroup(Brick; "No.", Name, Balance)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnInsert()
|
||||
begin
|
||||
if "No." = '' then begin
|
||||
CustomerMgt.TestNoSeries();
|
||||
CustomerMgt.InitNo(Rec);
|
||||
end;
|
||||
|
||||
"Last Date Modified" := Today();
|
||||
end;
|
||||
|
||||
trigger OnModify()
|
||||
begin
|
||||
"Last Date Modified" := Today();
|
||||
end;
|
||||
|
||||
trigger OnDelete()
|
||||
var
|
||||
CustomerLedgerEntry: Record "Cust. Ledger Entry";
|
||||
begin
|
||||
CustomerLedgerEntry.SetRange("Customer No.", "No.");
|
||||
if not CustomerLedgerEntry.IsEmpty() then
|
||||
Error('Cannot delete customer %1 with ledger entries', "No.");
|
||||
end;
|
||||
|
||||
trigger OnRename()
|
||||
begin
|
||||
"Last Date Modified" := Today();
|
||||
end;
|
||||
|
||||
var
|
||||
CustomerMgt: Codeunit CustomerMgt;
|
||||
|
||||
procedure UpdateSearchName()
|
||||
begin
|
||||
"Search Name" := UpperCase(Name);
|
||||
end;
|
||||
|
||||
procedure CheckCreditLimit()
|
||||
var
|
||||
CreditLimitExceeded: Boolean;
|
||||
begin
|
||||
CalcFields(Balance);
|
||||
CreditLimitExceeded := (Balance > "Credit Limit") and ("Credit Limit" <> 0);
|
||||
|
||||
if CreditLimitExceeded then
|
||||
Message('Credit limit exceeded for customer %1', "No.");
|
||||
end;
|
||||
|
||||
procedure GetDisplayName(): Text
|
||||
begin
|
||||
exit(Name + ' (' + "No." + ')');
|
||||
end;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
|
||||
@pytest.mark.al
|
||||
class TestALLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
|
||||
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that AL Language Server can find symbols in the test repository."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
|
||||
# Check for table symbols - AL returns full object names like 'Table 50000 "TEST Customer"'
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, 'Table 50000 "TEST Customer"'), "TEST Customer table not found in symbol tree"
|
||||
|
||||
# Check for page symbols
|
||||
assert SymbolUtils.symbol_tree_contains_name(
|
||||
symbols, 'Page 50001 "TEST Customer Card"'
|
||||
), "TEST Customer Card page not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(
|
||||
symbols, 'Page 50002 "TEST Customer List"'
|
||||
), "TEST Customer List page not found in symbol tree"
|
||||
|
||||
# Check for codeunit symbols
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "Codeunit 50000 CustomerMgt"), "CustomerMgt codeunit not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(
|
||||
symbols, "Codeunit 50001 PaymentProcessorImpl"
|
||||
), "PaymentProcessorImpl codeunit not found in symbol tree"
|
||||
|
||||
# Check for enum symbol
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "Enum 50000 CustomerType"), "CustomerType enum not found in symbol tree"
|
||||
|
||||
# Check for interface symbol
|
||||
assert SymbolUtils.symbol_tree_contains_name(
|
||||
symbols, "Interface IPaymentProcessor"
|
||||
), "IPaymentProcessor interface not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
|
||||
def test_find_table_fields(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that AL Language Server can find fields within a table."""
|
||||
file_path = os.path.join("src", "Tables", "Customer.Table.al")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
|
||||
# AL tables should have their fields as child symbols
|
||||
customer_table = None
|
||||
all_symbols, root_symbols = symbols
|
||||
for sym in root_symbols:
|
||||
if "TEST Customer" in sym.get("name", ""):
|
||||
customer_table = sym
|
||||
break
|
||||
|
||||
assert customer_table is not None, "Could not find TEST Customer table symbol"
|
||||
|
||||
# Check for field symbols (AL nests fields under a "fields" group)
|
||||
if "children" in customer_table:
|
||||
# Find the fields group
|
||||
fields_group = None
|
||||
for child in customer_table.get("children", []):
|
||||
if child.get("name") == "fields":
|
||||
fields_group = child
|
||||
break
|
||||
|
||||
assert fields_group is not None, "Fields group not found in Customer table"
|
||||
|
||||
# Check actual field names
|
||||
if "children" in fields_group:
|
||||
field_names = [child.get("name", "") for child in fields_group.get("children", [])]
|
||||
assert any("Name" in name for name in field_names), f"Name field not found. Fields: {field_names}"
|
||||
assert any("Balance" in name for name in field_names), f"Balance field not found. Fields: {field_names}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
|
||||
def test_find_procedures(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that AL Language Server can find procedures in codeunits."""
|
||||
file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
|
||||
# Find the codeunit symbol - AL returns 'Codeunit 50000 CustomerMgt'
|
||||
codeunit_symbol = None
|
||||
all_symbols, root_symbols = symbols
|
||||
for sym in root_symbols:
|
||||
if "CustomerMgt" in sym.get("name", ""):
|
||||
codeunit_symbol = sym
|
||||
break
|
||||
|
||||
assert codeunit_symbol is not None, "Could not find CustomerMgt codeunit symbol"
|
||||
|
||||
# Check for procedure symbols (if hierarchical)
|
||||
if "children" in codeunit_symbol:
|
||||
procedure_names = [child.get("name", "") for child in codeunit_symbol.get("children", [])]
|
||||
assert any("CreateCustomer" in name for name in procedure_names), "CreateCustomer procedure not found"
|
||||
# Note: UpdateCustomerBalance doesn't exist in our test repo, check for actual procedures
|
||||
assert any("TestNoSeries" in name for name in procedure_names), "TestNoSeries procedure not found"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
|
||||
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that AL Language Server can find references to symbols."""
|
||||
# Find references to the Customer table from the CustomerMgt codeunit
|
||||
table_file = os.path.join("src", "Tables", "Customer.Table.al")
|
||||
symbols = language_server.request_document_symbols(table_file)
|
||||
|
||||
# Find the Customer table symbol
|
||||
customer_symbol = None
|
||||
all_symbols, root_symbols = symbols
|
||||
for sym in root_symbols:
|
||||
if "TEST Customer" in sym.get("name", ""):
|
||||
customer_symbol = sym
|
||||
break
|
||||
|
||||
if customer_symbol and "selectionRange" in customer_symbol:
|
||||
sel_start = customer_symbol["selectionRange"]["start"]
|
||||
refs = language_server.request_references(table_file, sel_start["line"], sel_start["character"])
|
||||
|
||||
# The Customer table should be referenced in CustomerMgt.Codeunit.al
|
||||
assert any(
|
||||
"CustomerMgt.Codeunit.al" in ref.get("relativePath", "") for ref in refs
|
||||
), "Customer table should be referenced in CustomerMgt.Codeunit.al"
|
||||
|
||||
# It should also be referenced in CustomerCard.Page.al
|
||||
assert any(
|
||||
"CustomerCard.Page.al" in ref.get("relativePath", "") for ref in refs
|
||||
), "Customer table should be referenced in CustomerCard.Page.al"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
|
||||
def test_cross_file_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that AL Language Server can handle cross-file symbol relationships."""
|
||||
# Get all symbols to verify cross-file visibility
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
|
||||
# Count how many AL-specific symbols we found
|
||||
al_symbols = []
|
||||
|
||||
def collect_symbols(syms):
|
||||
for sym in syms:
|
||||
if isinstance(sym, dict):
|
||||
name = sym.get("name", "")
|
||||
# Look for AL object names (Table, Page, Codeunit, etc.)
|
||||
if any(keyword in name for keyword in ["Table", "Page", "Codeunit", "Enum", "Interface"]):
|
||||
al_symbols.append(name)
|
||||
if "children" in sym:
|
||||
collect_symbols(sym["children"])
|
||||
|
||||
collect_symbols(symbols)
|
||||
|
||||
# We should find symbols from multiple files
|
||||
assert len(al_symbols) >= 5, f"Expected at least 5 AL object symbols, found {len(al_symbols)}: {al_symbols}"
|
||||
|
||||
# Verify we have symbols from different AL object types
|
||||
has_table = any("Table" in s for s in al_symbols)
|
||||
has_page = any("Page" in s for s in al_symbols)
|
||||
has_codeunit = any("Codeunit" in s for s in al_symbols)
|
||||
|
||||
assert has_table, f"No Table symbols found in: {al_symbols}"
|
||||
assert has_page, f"No Page symbols found in: {al_symbols}"
|
||||
assert has_codeunit, f"No Codeunit symbols found in: {al_symbols}"
|
||||
Reference in new issue
Block a user