From de36f710dfefe21c000c8d1350e25df0634024f0 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 13 Feb 2026 14:55:57 +0100 Subject: [PATCH] Add project files. --- OdooAPI.sln | 25 ++ .../DataAccess/Odoo/Dtos/OdooProductDto.cs | 7 + .../DataAccess/Odoo/Dtos/OdooSupplierDto.cs | 10 + OdooAPI/DataAccess/Odoo/OdooLogin.cs | 63 +++++ .../Repositories/OdooProductRepository.cs | 249 ++++++++++++++++++ .../Repositories/ArtikelRepository.cs | 15 ++ .../Repositories/LevArtikelRepository.cs | 32 +++ .../Repositories/LeverancierRepository.cs | 27 ++ OdooAPI/DataAccess/Xml/XmlItemDto.cs | 10 + OdooAPI/DataAccess/Xml/XmlItemMapper.cs | 32 +++ OdooAPI/DataAccess/Xml/XmlSupplierReader.cs | 38 +++ OdooAPI/Database/AppDbContext.cs | 41 +++ OdooAPI/Database/ArtikelRepository.cs | 21 ++ OdooAPI/Database/DbInitializer.cs | 10 + OdooAPI/Database/Tabellen/Artkel.cs | 16 ++ OdooAPI/Database/Tabellen/LevArtikel.cs | 24 ++ OdooAPI/Database/Tabellen/Leverancier.cs | 10 + OdooAPI/Database/Tabellen/Marge.cs | 10 + OdooAPI/Import/OdooSycnProcessor.cs | 12 + OdooAPI/Import/SupplierImportProcessor.cs | 119 +++++++++ OdooAPI/Import/XMLImportRunner.cs | 19 ++ OdooAPI/Import/XmlImporter.cs | 41 +++ OdooAPI/OdooAPI.csproj | 20 ++ OdooAPI/Program.cs | 44 ++++ OdooAPI/Services/ArtikelMatcher.cs | 13 + OdooAPI/Services/AvailabilityRules.cs | 9 + OdooAPI/Services/EanValidator.cs | 12 + OdooAPI/Services/Helpers/DecimalHelper.cs | 18 ++ OdooAPI/Services/Helpers/Keybuilder.cs | 9 + OdooAPI/Services/Helpers/Normalizer.cs | 12 + OdooAPI/Services/LeverancierScanner.cs | 14 + OdooAPI/Services/MatchResult.cs | 12 + OdooAPI/Services/OdooSyncApp.cs | 57 ++++ OdooAPI/Services/OdooXmlMatchService.cs | 27 ++ OdooAPI/Services/PriceComparer.cs | 9 + OdooAPI/Services/SupplierMatchService.cs | 50 ++++ 36 files changed, 1137 insertions(+) create mode 100644 OdooAPI.sln create mode 100644 OdooAPI/DataAccess/Odoo/Dtos/OdooProductDto.cs create mode 100644 OdooAPI/DataAccess/Odoo/Dtos/OdooSupplierDto.cs create mode 100644 OdooAPI/DataAccess/Odoo/OdooLogin.cs create mode 100644 OdooAPI/DataAccess/Odoo/Repositories/OdooProductRepository.cs create mode 100644 OdooAPI/DataAccess/Repositories/ArtikelRepository.cs create mode 100644 OdooAPI/DataAccess/Repositories/LevArtikelRepository.cs create mode 100644 OdooAPI/DataAccess/Repositories/LeverancierRepository.cs create mode 100644 OdooAPI/DataAccess/Xml/XmlItemDto.cs create mode 100644 OdooAPI/DataAccess/Xml/XmlItemMapper.cs create mode 100644 OdooAPI/DataAccess/Xml/XmlSupplierReader.cs create mode 100644 OdooAPI/Database/AppDbContext.cs create mode 100644 OdooAPI/Database/ArtikelRepository.cs create mode 100644 OdooAPI/Database/DbInitializer.cs create mode 100644 OdooAPI/Database/Tabellen/Artkel.cs create mode 100644 OdooAPI/Database/Tabellen/LevArtikel.cs create mode 100644 OdooAPI/Database/Tabellen/Leverancier.cs create mode 100644 OdooAPI/Database/Tabellen/Marge.cs create mode 100644 OdooAPI/Import/OdooSycnProcessor.cs create mode 100644 OdooAPI/Import/SupplierImportProcessor.cs create mode 100644 OdooAPI/Import/XMLImportRunner.cs create mode 100644 OdooAPI/Import/XmlImporter.cs create mode 100644 OdooAPI/OdooAPI.csproj create mode 100644 OdooAPI/Program.cs create mode 100644 OdooAPI/Services/ArtikelMatcher.cs create mode 100644 OdooAPI/Services/AvailabilityRules.cs create mode 100644 OdooAPI/Services/EanValidator.cs create mode 100644 OdooAPI/Services/Helpers/DecimalHelper.cs create mode 100644 OdooAPI/Services/Helpers/Keybuilder.cs create mode 100644 OdooAPI/Services/Helpers/Normalizer.cs create mode 100644 OdooAPI/Services/LeverancierScanner.cs create mode 100644 OdooAPI/Services/MatchResult.cs create mode 100644 OdooAPI/Services/OdooSyncApp.cs create mode 100644 OdooAPI/Services/OdooXmlMatchService.cs create mode 100644 OdooAPI/Services/PriceComparer.cs create mode 100644 OdooAPI/Services/SupplierMatchService.cs diff --git a/OdooAPI.sln b/OdooAPI.sln new file mode 100644 index 0000000..00b791c --- /dev/null +++ b/OdooAPI.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36623.8 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OdooAPI", "OdooAPI\OdooAPI.csproj", "{AC50ACEB-8209-4330-B8BA-DB84922F8226}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AC50ACEB-8209-4330-B8BA-DB84922F8226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC50ACEB-8209-4330-B8BA-DB84922F8226}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC50ACEB-8209-4330-B8BA-DB84922F8226}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC50ACEB-8209-4330-B8BA-DB84922F8226}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B1E1DA3F-BBFA-44B2-9C16-F05361CE4C27} + EndGlobalSection +EndGlobal diff --git a/OdooAPI/DataAccess/Odoo/Dtos/OdooProductDto.cs b/OdooAPI/DataAccess/Odoo/Dtos/OdooProductDto.cs new file mode 100644 index 0000000..647f5fc --- /dev/null +++ b/OdooAPI/DataAccess/Odoo/Dtos/OdooProductDto.cs @@ -0,0 +1,7 @@ +namespace OdooAPI.DataAccess.Odoo.Dtos; + +public class OdooProductDto +{ + public int Id { get; set; } + public string DefaultCode { get; set; } = ""; +} diff --git a/OdooAPI/DataAccess/Odoo/Dtos/OdooSupplierDto.cs b/OdooAPI/DataAccess/Odoo/Dtos/OdooSupplierDto.cs new file mode 100644 index 0000000..cbb087a --- /dev/null +++ b/OdooAPI/DataAccess/Odoo/Dtos/OdooSupplierDto.cs @@ -0,0 +1,10 @@ +namespace OdooAPI.DataAccess.Odoo.Dtos; + +public class OdooSupplierDto +{ + public int Id { get; set; } + + public int PartnerId { get; set; } + + public string Name { get; set; } = ""; +} diff --git a/OdooAPI/DataAccess/Odoo/OdooLogin.cs b/OdooAPI/DataAccess/Odoo/OdooLogin.cs new file mode 100644 index 0000000..779591f --- /dev/null +++ b/OdooAPI/DataAccess/Odoo/OdooLogin.cs @@ -0,0 +1,63 @@ +using System.Net.Http; +using System.Text; +using System.Text.Json; + +namespace OdooAPI.DataAccess.Odoo; + +public class OdooLogin +{ + private const string Url = "https://odooapi1.odoo.com/jsonrpc"; + + private const string Database = "odooapi1"; + private const string Username = "luca@corsie.nl"; + private const string Password = "XS82ns23!"; + + private readonly HttpClient _http; + + public OdooLogin(HttpClient http) + { + _http = http; + } + + public async Task LoginAsync() + { + var payload = new + { + jsonrpc = "2.0", + method = "call", + id = 1, + @params = new + { + service = "common", + method = "login", + args = new object[] + { + Database, + Username, + Password + } + } + }; + + var content = new StringContent( + JsonSerializer.Serialize(payload), + Encoding.UTF8, + "application/json"); + + var response = await _http.PostAsync(Url, content); + + var json = await response.Content.ReadAsStringAsync(); + + Console.WriteLine("LOGIN RESPONSE:"); + Console.WriteLine(json); + + using var doc = JsonDocument.Parse(json); + + if (!doc.RootElement.TryGetProperty("result", out var result)) + return null; + + return result.ValueKind == JsonValueKind.Number + ? result.GetInt32() + : null; + } +} diff --git a/OdooAPI/DataAccess/Odoo/Repositories/OdooProductRepository.cs b/OdooAPI/DataAccess/Odoo/Repositories/OdooProductRepository.cs new file mode 100644 index 0000000..7cb4a9e --- /dev/null +++ b/OdooAPI/DataAccess/Odoo/Repositories/OdooProductRepository.cs @@ -0,0 +1,249 @@ +using System.Text; +using System.Text.Json; + +namespace OdooAPI.DataAccess.Odoo.Repositories; + +public class OdooProductRepository +{ + private readonly HttpClient _http; + + private const string Url = "https://odooapi1.odoo.com/jsonrpc"; + private const string Database = "odooapi1"; + private const string Username = "luca@corsie.nl"; + private const string Password = "XS82ns23!"; + + private int _uid; + + // ================================ + // PROPERTY (fix voor jouw error) + // ================================ + public int Uid => _uid; + + public OdooProductRepository(HttpClient http) + { + _http = http; + } + + // ========================================= + // LOGIN + // ========================================= + public async Task LoginAsync() + { + var result = await CallAsync( + "common", + "login", + new object[] { Database, Username, Password }); + + _uid = result.GetInt32(); + } + + // ========================================= + // PRODUCTEN OPHALEN (fix voor jouw error) + // ========================================= + public async Task> GetAllProductsAsync() + { + var result = await CallAsync( + "object", + "execute_kw", + new object[] + { + Database, _uid, Password, + "product.product", + "search_read", + new object[] { new object[] { } } + }, + new { fields = new[] { "default_code" } } + ); + + var list = new List(); + + foreach (var p in result.EnumerateArray()) + { + if (p.TryGetProperty("default_code", out var code) && + code.ValueKind == JsonValueKind.String) + { + list.Add(code.GetString()!); + } + } + + return list; + } + + // ========================================= + // UPSERT SUPPLIER (CREATE of UPDATE) + // ========================================= + public async Task UpsertSupplierAsync( + string defaultCode, + string supplierName, + string leverancierProductCode, + decimal price, + int delay) + { + var partnerId = await GetPartnerIdAsync(supplierName); + if (partnerId == null) + throw new Exception($"Supplier '{supplierName}' niet gevonden."); + + // product template ophalen (VERPLICHT in Odoo) + var tmplId = await GetProductTemplateIdAsync(defaultCode); + if (tmplId == null) + throw new Exception($"Product template niet gevonden voor {defaultCode}"); + + var existingId = + await GetExistingSupplierInfoId(partnerId.Value, leverancierProductCode); + + var values = new + { + partner_id = partnerId, + product_tmpl_id = tmplId, // ⭐ BELANGRIJKSTE FIX + product_code = leverancierProductCode, + price = price, + delay = delay, + product_uom_id = 1 + }; + + if (existingId != null) + { + Console.WriteLine(" ␦ UPDATE in Odoo"); + + await CallAsync( + "object", + "execute_kw", + new object[] + { + Database,_uid,Password, + "product.supplierinfo", + "write", + new object[] { new [] { existingId }, values } + }); + } + else + { + Console.WriteLine(" ␦ CREATE in Odoo"); + + await CallAsync( + "object", + "execute_kw", + new object[] + { + Database,_uid,Password, + "product.supplierinfo", + "create", + new object[] { values } + }); + } + } + + // ========================================= + // TEMPLATE ID (nieuw – verplicht) + // ========================================= + private async Task GetProductTemplateIdAsync(string defaultCode) + { + var result = await CallAsync( + "object", + "execute_kw", + new object[] + { + Database,_uid,Password, + "product.product", + "search_read", + new object[] + { + new object[] + { + new object[] { "default_code", "=", defaultCode } + } + } + }, + new { fields = new[] { "product_tmpl_id" }, limit = 1 } + ); + + foreach (var r in result.EnumerateArray()) + { + return r.GetProperty("product_tmpl_id")[0].GetInt32(); + } + + return null; + } + + private async Task GetExistingSupplierInfoId(int partnerId, string code) + { + var result = await CallAsync( + "object", + "execute_kw", + new object[] + { + Database,_uid,Password, + "product.supplierinfo", + "search", + new object[] + { + new object[] + { + new object[] { "partner_id", "=", partnerId }, + new object[] { "product_code", "=", code } + } + } + }); + + var ids = result.EnumerateArray().ToList(); + + return ids.Count == 0 ? null : ids[0].GetInt32(); + } + + private async Task GetPartnerIdAsync(string name) + { + var result = await CallAsync( + "object", + "execute_kw", + new object[] + { + Database,_uid,Password, + "res.partner", + "search", + new object[] + { + new object[] + { + new object[] { "name", "=", name } + } + } + }); + + var ids = result.EnumerateArray().ToList(); + + return ids.Count == 0 ? null : ids[0].GetInt32(); + } + + // ========================================= + // CORE JSON RPC + // ========================================= + private async Task CallAsync( + string service, + string method, + object[] args, + object? kwargs = null) + { + var payload = new + { + jsonrpc = "2.0", + method = "call", + id = 1, + @params = new { service, method, args, kwargs } + }; + + var json = JsonSerializer.Serialize(payload); + + var response = await _http.PostAsync( + Url, + new StringContent(json, Encoding.UTF8, "application/json")); + + var content = await response.Content.ReadAsStringAsync(); + + using var doc = JsonDocument.Parse(content); + + if (doc.RootElement.TryGetProperty("error", out var error)) + throw new Exception(error.ToString()); + + return doc.RootElement.GetProperty("result").Clone(); + } +} diff --git a/OdooAPI/DataAccess/Repositories/ArtikelRepository.cs b/OdooAPI/DataAccess/Repositories/ArtikelRepository.cs new file mode 100644 index 0000000..ca2a919 --- /dev/null +++ b/OdooAPI/DataAccess/Repositories/ArtikelRepository.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using OdooAPI.Database; + +public class ArtikelRepository +{ + public async Task> GetSupplierNamesAsync(int artikelId) + { + using var db = new AppDbContext(); + + return await db.LevArtikelen + .Where(x => x.ArtikelId == artikelId) + .Select(x => x.Leverancier.Naam) + .ToListAsync(); + } +} diff --git a/OdooAPI/DataAccess/Repositories/LevArtikelRepository.cs b/OdooAPI/DataAccess/Repositories/LevArtikelRepository.cs new file mode 100644 index 0000000..eeb9772 --- /dev/null +++ b/OdooAPI/DataAccess/Repositories/LevArtikelRepository.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using OdooAPI.Database; +using OdooAPI.Database.Tabellen; + +namespace OdooAPI.DataAccess.Repositories; + +public class LevArtikelRepository +{ + private readonly AppDbContext _db; + + public LevArtikelRepository(AppDbContext db) + { + _db = db; + } + + public Dictionary<(int, int), LevArtikel> GetAllLookup() + { + return _db.LevArtikelen + .AsNoTracking() + .ToDictionary(x => (x.LeverancierId, x.ArtikelId), x => x); + } + + public void AddRange(List items) + { + _db.LevArtikelen.AddRange(items); + } + + public void UpdateRange(List items) + { + _db.LevArtikelen.UpdateRange(items); + } +} diff --git a/OdooAPI/DataAccess/Repositories/LeverancierRepository.cs b/OdooAPI/DataAccess/Repositories/LeverancierRepository.cs new file mode 100644 index 0000000..372ab6a --- /dev/null +++ b/OdooAPI/DataAccess/Repositories/LeverancierRepository.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using OdooAPI.Database; +using OdooAPI.Database.Tabellen; + +namespace OdooAPI.DataAccess.Repositories; + +public class LeverancierRepository +{ + private readonly AppDbContext _db; + + public LeverancierRepository(AppDbContext db) + { + _db = db; + } + + public Leverancier? GetByNaam(string naam) + { + return _db.Leveranciers.FirstOrDefault(x => x.Naam == naam); + } + + public Leverancier Add(string naam) + { + var lev = new Leverancier { Naam = naam }; + _db.Leveranciers.Add(lev); + return lev; + } +} diff --git a/OdooAPI/DataAccess/Xml/XmlItemDto.cs b/OdooAPI/DataAccess/Xml/XmlItemDto.cs new file mode 100644 index 0000000..a323eb4 --- /dev/null +++ b/OdooAPI/DataAccess/Xml/XmlItemDto.cs @@ -0,0 +1,10 @@ +namespace OdooAPI.DataAccess.Xml; + +public class XmlItemDto +{ + public string VendorId { get; set; } = ""; + public string Naam { get; set; } = ""; + public decimal Prijs { get; set; } + public int Stock { get; set; } + public string? Ean { get; set; } +} diff --git a/OdooAPI/DataAccess/Xml/XmlItemMapper.cs b/OdooAPI/DataAccess/Xml/XmlItemMapper.cs new file mode 100644 index 0000000..1e0b1e0 --- /dev/null +++ b/OdooAPI/DataAccess/Xml/XmlItemMapper.cs @@ -0,0 +1,32 @@ +using OdooAPI.Database.Tabellen; + +namespace OdooAPI.DataAccess.Xml; + +public class XmlItemMapper +{ + public Artikel MapToArtikel(XmlItemDto dto) + { + return new Artikel + { + ArtikelNummer = dto.VendorId, + Naam = dto.Naam, + Ean = dto.Ean, + InOdoo = false + }; + } + + public LevArtikel MapToLevArtikel(XmlItemDto dto, int leverancierId, int artikelId) + { + return new LevArtikel + { + LeverancierId = leverancierId, + ArtikelId = artikelId, + ArtikelNummerLev = dto.VendorId, + Inkoopprijs = dto.Prijs, + Levertijd = 1, + Omschrijving = "", + Leverbaar = dto.Stock > 0, + Korting = null + }; + } +} diff --git a/OdooAPI/DataAccess/Xml/XmlSupplierReader.cs b/OdooAPI/DataAccess/Xml/XmlSupplierReader.cs new file mode 100644 index 0000000..a744baa --- /dev/null +++ b/OdooAPI/DataAccess/Xml/XmlSupplierReader.cs @@ -0,0 +1,38 @@ +using System.Xml.Linq; +using OdooAPI.Services.Helpers; + +namespace OdooAPI.DataAccess.Xml; + +public class XmlSupplierReader +{ + public List ReadFolder(string supplierFolder) + { + var result = new List(); + + var files = Directory.GetFiles(supplierFolder, "*.xml"); + + foreach (var file in files) + { + result.AddRange(ReadFile(file)); + } + + return result; + } + + private IEnumerable ReadFile(string filePath) + { + var doc = XDocument.Load(filePath); + + foreach (var item in doc.Descendants("item")) + { + yield return new XmlItemDto + { + VendorId = item.Element("vendor_id")?.Value ?? "", + Naam = item.Element("long_desc")?.Value ?? "", + Prijs = DecimalHelper.Parse(item.Element("price")?.Value), + Stock = int.TryParse(item.Element("stock")?.Value, out var s) ? s : 0, + Ean = item.Element("EAN_code")?.Value + }; + } + } +} diff --git a/OdooAPI/Database/AppDbContext.cs b/OdooAPI/Database/AppDbContext.cs new file mode 100644 index 0000000..6c03051 --- /dev/null +++ b/OdooAPI/Database/AppDbContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using OdooAPI.Database.Tabellen; + +namespace OdooAPI.Database; + +public class AppDbContext : DbContext +{ + public DbSet Artikelen => Set(); + public DbSet Leveranciers => Set(); + public DbSet LevArtikelen => Set(); + public DbSet Marges => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + var path = Path.Combine(AppContext.BaseDirectory, "database.db"); + options.UseSqlite($"Data Source={path}"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasIndex(x => x.ArtikelNummer) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(x => new { x.LeverancierId, x.ArtikelId }) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(x => x.Artikel) + .WithMany(x => x.LevArtikelen) + .HasForeignKey(x => x.ArtikelId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(x => x.Leverancier) + .WithMany(x => x.LevArtikelen) + .HasForeignKey(x => x.LeverancierId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/OdooAPI/Database/ArtikelRepository.cs b/OdooAPI/Database/ArtikelRepository.cs new file mode 100644 index 0000000..7514346 --- /dev/null +++ b/OdooAPI/Database/ArtikelRepository.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace OdooAPI.Database; + +public class ArtikelRepository +{ + private readonly AppDbContext _db; + + public ArtikelRepository(AppDbContext db) + { + _db = db; + } + + public async Task> GetAllArtikelNummersAsync() + { + return await _db.Artikelen + .Where(a => a.ArtikelNummer != null) + .Select(a => a.ArtikelNummer!) + .ToListAsync(); + } +} diff --git a/OdooAPI/Database/DbInitializer.cs b/OdooAPI/Database/DbInitializer.cs new file mode 100644 index 0000000..d02e745 --- /dev/null +++ b/OdooAPI/Database/DbInitializer.cs @@ -0,0 +1,10 @@ +namespace OdooAPI.Database; + +public static class DbInitializer +{ + public static void Initialize() + { + using var db = new AppDbContext(); + db.Database.EnsureCreated(); + } +} diff --git a/OdooAPI/Database/Tabellen/Artkel.cs b/OdooAPI/Database/Tabellen/Artkel.cs new file mode 100644 index 0000000..2a87d88 --- /dev/null +++ b/OdooAPI/Database/Tabellen/Artkel.cs @@ -0,0 +1,16 @@ +namespace OdooAPI.Database.Tabellen; + +public class Artikel +{ + public int Id { get; set; } + + public string Naam { get; set; } = ""; + + public string ArtikelNummer { get; set; } = ""; + + public string? Ean { get; set; } + + public bool InOdoo { get; set; } + + public List LevArtikelen { get; set; } = new(); +} diff --git a/OdooAPI/Database/Tabellen/LevArtikel.cs b/OdooAPI/Database/Tabellen/LevArtikel.cs new file mode 100644 index 0000000..a825479 --- /dev/null +++ b/OdooAPI/Database/Tabellen/LevArtikel.cs @@ -0,0 +1,24 @@ +namespace OdooAPI.Database.Tabellen; + +public class LevArtikel +{ + public int Id { get; set; } + + public int LeverancierId { get; set; } + public Leverancier Leverancier { get; set; } = null!; + + public int ArtikelId { get; set; } + public Artikel Artikel { get; set; } = null!; + + public string ArtikelNummerLev { get; set; } = ""; + + public decimal Inkoopprijs { get; set; } + + public int Levertijd { get; set; } = 1; + + public string Omschrijving { get; set; } = ""; + + public bool Leverbaar { get; set; } + + public decimal? Korting { get; set; } +} diff --git a/OdooAPI/Database/Tabellen/Leverancier.cs b/OdooAPI/Database/Tabellen/Leverancier.cs new file mode 100644 index 0000000..607bc65 --- /dev/null +++ b/OdooAPI/Database/Tabellen/Leverancier.cs @@ -0,0 +1,10 @@ +namespace OdooAPI.Database.Tabellen; + +public class Leverancier +{ + public int Id { get; set; } + + public string Naam { get; set; } = ""; + + public List LevArtikelen { get; set; } = new(); +} diff --git a/OdooAPI/Database/Tabellen/Marge.cs b/OdooAPI/Database/Tabellen/Marge.cs new file mode 100644 index 0000000..8920c59 --- /dev/null +++ b/OdooAPI/Database/Tabellen/Marge.cs @@ -0,0 +1,10 @@ +namespace OdooAPI.Database.Tabellen; + +public class Marge +{ + public int Id { get; set; } + + public int ArtikelId { get; set; } + + public decimal Percentage { get; set; } +} diff --git a/OdooAPI/Import/OdooSycnProcessor.cs b/OdooAPI/Import/OdooSycnProcessor.cs new file mode 100644 index 0000000..32c56a9 --- /dev/null +++ b/OdooAPI/Import/OdooSycnProcessor.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OdooAPI.Import +{ + internal class OdooSycnProcessor + { + } +} diff --git a/OdooAPI/Import/SupplierImportProcessor.cs b/OdooAPI/Import/SupplierImportProcessor.cs new file mode 100644 index 0000000..65c3f7d --- /dev/null +++ b/OdooAPI/Import/SupplierImportProcessor.cs @@ -0,0 +1,119 @@ +using OdooAPI.Database; +using OdooAPI.Database.Tabellen; +using OdooAPI.DataAccess.Xml; + +namespace OdooAPI.Import; + +public class SupplierImportProcessor +{ + private readonly AppDbContext _db; + + public SupplierImportProcessor(AppDbContext db) + { + _db = db; + } + + public void Process(string supplierFolder) + { + var supplierName = Path.GetFileName(supplierFolder); + + Console.WriteLine($"Start import: {supplierName}"); + + var leverancier = _db.Leveranciers + .FirstOrDefault(x => x.Naam == supplierName); + + if (leverancier == null) + { + leverancier = new Leverancier { Naam = supplierName }; + _db.Leveranciers.Add(leverancier); + _db.SaveChanges(); + Console.WriteLine("Nieuwe leverancier aangemaakt"); + } + + var reader = new XmlSupplierReader(); + var items = reader.ReadFolder(supplierFolder); + + Console.WriteLine($"XML items gevonden: {items.Count}"); + + var bestaandeArtikelen = _db.Artikelen + .ToDictionary(x => x.ArtikelNummer); + + int nieuweArtikelen = 0; + + foreach (var item in items) + { + if (!bestaandeArtikelen.ContainsKey(item.VendorId)) + { + var artikel = new Artikel + { + Naam = item.Naam, + ArtikelNummer = item.VendorId, + Ean = item.Ean, + InOdoo = false + }; + + _db.Artikelen.Add(artikel); + bestaandeArtikelen[item.VendorId] = artikel; + + nieuweArtikelen++; + } + } + + _db.SaveChanges(); + + Console.WriteLine($"Nieuwe artikelen: {nieuweArtikelen}"); + + var bestaandeLevArtikelen = _db.LevArtikelen + .Where(x => x.LeverancierId == leverancier.Id) + .ToDictionary(x => x.ArtikelId); + + var gezienArtikelIds = new HashSet(); + + int nieuweLev = 0; + int updates = 0; + + foreach (var item in items) + { + var artikel = bestaandeArtikelen[item.VendorId]; + + if (!bestaandeLevArtikelen.TryGetValue(artikel.Id, out var levArtikel)) + { + levArtikel = new LevArtikel + { + LeverancierId = leverancier.Id, + ArtikelId = artikel.Id, + ArtikelNummerLev = item.VendorId, + Inkoopprijs = item.Prijs, + Levertijd = 1, + Leverbaar = true, + Omschrijving = "", + Korting = null + }; + + _db.LevArtikelen.Add(levArtikel); + nieuweLev++; + } + else + { + levArtikel.Inkoopprijs = item.Prijs; + levArtikel.Leverbaar = true; + updates++; + } + + gezienArtikelIds.Add(artikel.Id); + } + + foreach (var lev in bestaandeLevArtikelen.Values) + { + if (!gezienArtikelIds.Contains(lev.ArtikelId)) + lev.Leverbaar = false; + } + + _db.SaveChanges(); + + Console.WriteLine($"Nieuwe leverancier-artikelen: {nieuweLev}"); + Console.WriteLine($"Updates: {updates}"); + Console.WriteLine($"Klaar met: {supplierName}"); + Console.WriteLine(); + } +} diff --git a/OdooAPI/Import/XMLImportRunner.cs b/OdooAPI/Import/XMLImportRunner.cs new file mode 100644 index 0000000..1c60be9 --- /dev/null +++ b/OdooAPI/Import/XMLImportRunner.cs @@ -0,0 +1,19 @@ +using OdooAPI.Database; + +namespace OdooAPI.Import; + +public class XmlImportRunner +{ + public void Run() + { + using var db = new AppDbContext(); + + + db.Database.EnsureCreated(); + + + + var importer = new XmlImporter(db); + importer.Run(); + } +} diff --git a/OdooAPI/Import/XmlImporter.cs b/OdooAPI/Import/XmlImporter.cs new file mode 100644 index 0000000..98be6e0 --- /dev/null +++ b/OdooAPI/Import/XmlImporter.cs @@ -0,0 +1,41 @@ +using OdooAPI.Database; +using OdooAPI.Services; +using System.IO; + +namespace OdooAPI.Import; + +public class XmlImporter +{ + private readonly AppDbContext _db; + + public XmlImporter(AppDbContext db) + { + _db = db; + } + + public void Run() + { + var baseDir = Path.Combine(AppContext.BaseDirectory, "Leveranciers"); + + Console.WriteLine($"Zoeken naar leveranciers in: {baseDir}"); + + if (!Directory.Exists(baseDir)) + { + Console.WriteLine("Leveranciers map niet gevonden"); + return; + } + + var scanner = new LeverancierScanner(); + var supplierFolders = scanner.GetSupplierFolders(baseDir); + + foreach (var folder in supplierFolders) + { + var name = Path.GetFileName(folder); + + Console.WriteLine($"Leverancier gevonden: {name}"); + + var processor = new SupplierImportProcessor(_db); + processor.Process(folder); + } + } +} diff --git a/OdooAPI/OdooAPI.csproj b/OdooAPI/OdooAPI.csproj new file mode 100644 index 0000000..775aaf1 --- /dev/null +++ b/OdooAPI/OdooAPI.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/OdooAPI/Program.cs b/OdooAPI/Program.cs new file mode 100644 index 0000000..6a867df --- /dev/null +++ b/OdooAPI/Program.cs @@ -0,0 +1,44 @@ +using OdooAPI.Import; +using OdooAPI.Services; + +internal class Program +{ + static async Task Main(string[] args) + { + try + { + // ===================================== + // 1️⃣ XML IMPORT + // ===================================== + Console.WriteLine("================================="); + Console.WriteLine("XML IMPORT START"); + Console.WriteLine("================================="); + + var xml = new XmlImportRunner(); + xml.Run(); + + Console.WriteLine("XML IMPORT KLAAR\n"); + + + // ===================================== + // 2️⃣ ODOO SYNC + // ===================================== + Console.WriteLine("================================="); + Console.WriteLine("ODOO SYNC START"); + Console.WriteLine("================================="); + + var app = new OdooSyncApp(); + await app.RunAsync(); + + Console.WriteLine("\nKlaar."); + } + catch (Exception ex) + { + Console.WriteLine("\nFOUT:"); + Console.WriteLine(ex); + } + + Console.WriteLine("\nDruk op een toets om te sluiten..."); + Console.ReadKey(); + } +} diff --git a/OdooAPI/Services/ArtikelMatcher.cs b/OdooAPI/Services/ArtikelMatcher.cs new file mode 100644 index 0000000..c0da877 --- /dev/null +++ b/OdooAPI/Services/ArtikelMatcher.cs @@ -0,0 +1,13 @@ +namespace OdooAPI.Services; + +public class ArtikelMatcher +{ + public bool IsMatch(string artikelNummerA, string artikelNummerB) + { + return string.Equals( + artikelNummerA?.Trim(), + artikelNummerB?.Trim(), + StringComparison.OrdinalIgnoreCase + ); + } +} diff --git a/OdooAPI/Services/AvailabilityRules.cs b/OdooAPI/Services/AvailabilityRules.cs new file mode 100644 index 0000000..710f600 --- /dev/null +++ b/OdooAPI/Services/AvailabilityRules.cs @@ -0,0 +1,9 @@ +namespace OdooAPI.Services; + +public static class AvailabilityRules +{ + public static bool IsAvailable(int stock) + { + return stock > 0; + } +} diff --git a/OdooAPI/Services/EanValidator.cs b/OdooAPI/Services/EanValidator.cs new file mode 100644 index 0000000..048f50f --- /dev/null +++ b/OdooAPI/Services/EanValidator.cs @@ -0,0 +1,12 @@ +namespace OdooAPI.Services; + +public static class EanValidator +{ + public static bool IsValid(string? ean) + { + if (string.IsNullOrWhiteSpace(ean)) + return false; + + return ean.All(char.IsDigit); + } +} diff --git a/OdooAPI/Services/Helpers/DecimalHelper.cs b/OdooAPI/Services/Helpers/DecimalHelper.cs new file mode 100644 index 0000000..c917d07 --- /dev/null +++ b/OdooAPI/Services/Helpers/DecimalHelper.cs @@ -0,0 +1,18 @@ +using System.Globalization; + +namespace OdooAPI.Services.Helpers; + +public static class DecimalHelper +{ + public static decimal Parse(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return 0m; + + value = value.Replace(",", "."); + + decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result); + + return result; + } +} diff --git a/OdooAPI/Services/Helpers/Keybuilder.cs b/OdooAPI/Services/Helpers/Keybuilder.cs new file mode 100644 index 0000000..792c43f --- /dev/null +++ b/OdooAPI/Services/Helpers/Keybuilder.cs @@ -0,0 +1,9 @@ +namespace OdooAPI.Services.Helpers; + +public static class Keybuilder +{ + public static string Build(string leverancier, string artikelNummer) + { + return $"{leverancier}_{artikelNummer}".ToLowerInvariant(); + } +} diff --git a/OdooAPI/Services/Helpers/Normalizer.cs b/OdooAPI/Services/Helpers/Normalizer.cs new file mode 100644 index 0000000..c44f671 --- /dev/null +++ b/OdooAPI/Services/Helpers/Normalizer.cs @@ -0,0 +1,12 @@ +namespace OdooAPI.Services.Helpers; + +public static class Normalizer +{ + public static string Clean(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + return value.Trim(); + } +} diff --git a/OdooAPI/Services/LeverancierScanner.cs b/OdooAPI/Services/LeverancierScanner.cs new file mode 100644 index 0000000..a45e62a --- /dev/null +++ b/OdooAPI/Services/LeverancierScanner.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace OdooAPI.Services; + +public class LeverancierScanner +{ + public IEnumerable GetSupplierFolders(string baseDir) + { + if (!Directory.Exists(baseDir)) + return Enumerable.Empty(); + + return Directory.GetDirectories(baseDir); + } +} diff --git a/OdooAPI/Services/MatchResult.cs b/OdooAPI/Services/MatchResult.cs new file mode 100644 index 0000000..55cab8a --- /dev/null +++ b/OdooAPI/Services/MatchResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OdooAPI.Services +{ + internal class MatchResult + { + } +} diff --git a/OdooAPI/Services/OdooSyncApp.cs b/OdooAPI/Services/OdooSyncApp.cs new file mode 100644 index 0000000..0f73712 --- /dev/null +++ b/OdooAPI/Services/OdooSyncApp.cs @@ -0,0 +1,57 @@ +using OdooAPI.DataAccess.Odoo.Repositories; +using OdooAPI.Database; + +namespace OdooAPI.Services; + +public class OdooSyncApp +{ + public async Task RunAsync() + { + Console.WriteLine("Inloggen in Odoo..."); + + var http = new HttpClient(); + var repo = new OdooProductRepository(http); + + await repo.LoginAsync(); + + Console.WriteLine($"Ingelogd bij Odoo (UID = {repo.Uid})"); + + // ========================================= + // PRODUCTEN OPHALEN + // ========================================= + + Console.WriteLine("\nProducten ophalen..."); + + var odooProducts = await repo.GetAllProductsAsync(); // List + + Console.WriteLine($"Odoo producten: {odooProducts.Count}"); + + var db = new AppDbContext(); + + var localCodes = db.LevArtikelen + .Select(x => x.ArtikelNummerLev) + .Distinct() + .ToList(); + + Console.WriteLine($"Database producten: {localCodes.Count}"); + + // ✅ BELANGRIJK: strings vergelijken (GEEN DefaultCode meer!) + var matches = odooProducts + .Where(code => localCodes.Contains(code)) + .ToList(); + + Console.WriteLine($"Matches: {matches.Count}"); + + // ========================================= + // SUPPLIER MATCHING + // ========================================= + + Console.WriteLine("\nSUPPLIER MATCHING"); + + var supplierMatch = new SupplierMatchService(db, repo); + + await supplierMatch.RunAsync(matches); + + Console.WriteLine("Klaar."); + } +} diff --git a/OdooAPI/Services/OdooXmlMatchService.cs b/OdooAPI/Services/OdooXmlMatchService.cs new file mode 100644 index 0000000..1035991 --- /dev/null +++ b/OdooAPI/Services/OdooXmlMatchService.cs @@ -0,0 +1,27 @@ +using OdooAPI.DataAccess.Odoo.Dtos; + +namespace OdooAPI.Services; + +public class OdooXmlMatchService +{ + public List GetMatchedCodes( + List odooProducts, + List localCodes) + { + var localSet = new HashSet(localCodes); + + var matches = odooProducts + .Where(p => + !string.IsNullOrEmpty(p.DefaultCode) && + localSet.Contains(p.DefaultCode)) + .Select(p => p.DefaultCode) + .ToList(); + + Console.WriteLine($"Aantal matches: {matches.Count}"); + + foreach (var m in matches) + Console.WriteLine($"MATCH: {m}"); + + return matches; + } +} diff --git a/OdooAPI/Services/PriceComparer.cs b/OdooAPI/Services/PriceComparer.cs new file mode 100644 index 0000000..5e4b479 --- /dev/null +++ b/OdooAPI/Services/PriceComparer.cs @@ -0,0 +1,9 @@ +namespace OdooAPI.Services; + +public static class PriceComparer +{ + public static bool HasChanged(decimal oldPrice, decimal newPrice) + { + return oldPrice != newPrice; + } +} diff --git a/OdooAPI/Services/SupplierMatchService.cs b/OdooAPI/Services/SupplierMatchService.cs new file mode 100644 index 0000000..c267fb9 --- /dev/null +++ b/OdooAPI/Services/SupplierMatchService.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; +using OdooAPI.Database; +using OdooAPI.DataAccess.Odoo.Repositories; + +namespace OdooAPI.Services; + +public class SupplierMatchService +{ + private readonly AppDbContext _db; + private readonly OdooProductRepository _repo; + + public SupplierMatchService(AppDbContext db, OdooProductRepository repo) + { + _db = db; + _repo = repo; + } + + public async Task RunAsync(List matchedCodes) + { + Console.WriteLine("SUPPLIER MATCHING"); + + foreach (var code in matchedCodes) + { + Console.WriteLine($"\nProduct: {code}"); + + var localSuppliers = await _db.LevArtikelen + .Where(x => x.ArtikelNummerLev == code) + .Select(x => new + { + Naam = x.Leverancier.Naam, + Code = x.ArtikelNummerLev, + Prijs = x.Inkoopprijs, + Delay = x.Leverbaar ? 1 : 0 + }) + .ToListAsync(); + + foreach (var s in localSuppliers) + { + Console.WriteLine($"UPSERT {s.Naam}"); + + await _repo.UpsertSupplierAsync( + code, + s.Naam, + s.Code, + s.Prijs, + s.Delay); + } + } + } +}