This commit is contained in:
che
2026-07-23 12:06:08 +05:00
parent 4cfc5d6ae6
commit dddf6516d3
47 changed files with 2253 additions and 94 deletions
+321
View File
@@ -0,0 +1,321 @@
use che_orm::__private::sqlx;
use che_tauri::{ApiError, TauriApi};
use serde::Deserialize;
#[derive(Debug, serde::Serialize)]
pub struct SyncContractsResult {
pub synced: usize,
pub pages: usize,
}
#[derive(Debug, Deserialize)]
struct ContractPage {
links: PageLinks,
results: Vec<RemoteContract>,
}
#[derive(Debug, Deserialize)]
struct PageLinks {
next: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RemoteContract {
id: i64,
name: String,
number: String,
#[serde(rename = "get_absolute_url")]
absolute_url: String,
#[serde(rename = "get_type")]
contract_type: String,
#[serde(rename = "get_category")]
category: String,
#[serde(rename = "get_amount_total")]
amount_total_display: String,
#[serde(rename = "get_amount_by_ds")]
amount_by_ds: String,
comment: String,
date: String,
#[serde(rename = "get_status")]
status_name: String,
status: String,
nds: String,
name_of_product: String,
counterparty: String,
amount: f64,
month_pay: Option<f64>,
avans_pay: String,
amount_total: f64,
estimate_nds_cost: f64,
estimate_nds_cert: f64,
#[serde(rename = "get_bill_cost")]
bill_cost: Option<RemoteBillCost>,
income_total: f64,
arrears: f64,
company: Option<RemoteCompany>,
project: Option<RemoteProject>,
frc: Option<RemoteFrc>,
get_employee: Option<RemoteEmployee>,
}
#[derive(Debug, Deserialize)]
struct RemoteBillCost {
#[serde(rename = "cost__sum")]
cost_sum: Option<f64>,
#[serde(rename = "paid__sum")]
paid_sum: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct RemoteCompany {
id: i64,
name: String,
}
#[derive(Debug, Deserialize)]
struct RemoteProject {
id: i64,
name: String,
full_name: String,
#[serde(rename = "get_short_name")]
short_name: String,
locality: Option<RemoteLocality>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum RemoteLocality {
Object { id: i64, name: String },
Id(i64),
}
#[derive(Debug, Deserialize)]
struct RemoteFrc {
id: i64,
name: String,
icon: Option<String>,
#[serde(rename = "get_balance")]
balance: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct RemoteEmployee {
id: i64,
name: String,
short_name: String,
avatar_small: Option<String>,
}
pub async fn sync_contracts(api: &TauriApi) -> Result<SyncContractsResult, ApiError> {
let state = api.state();
let token = state
.auth_token()
.ok_or_else(|| ApiError::new("not_authenticated", "authentication token is missing"))?;
let remote = state
.config
.remote
.as_ref()
.ok_or_else(|| ApiError::bad_request("sync requires [remote].base_url config"))?;
let client = reqwest::Client::new();
let mut next_url = Some(format!(
"{}/api/contract/?page_size=100",
remote.base_url.trim_end_matches('/')
));
let mut synced = 0;
let mut pages = 0;
while let Some(url) = next_url {
let response = client
.get(url)
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let detail = response.text().await.unwrap_or_default();
return Err(ApiError::new(
"remote_error",
format!("remote request failed with {status}: {detail}"),
));
}
let page = response.json::<ContractPage>().await?;
pages += 1;
for contract in &page.results {
upsert_contract_dependencies(api, contract).await?;
upsert_contract(api, contract).await?;
synced += 1;
}
next_url = page.links.next;
}
Ok(SyncContractsResult { synced, pages })
}
async fn upsert_contract_dependencies(
api: &TauriApi,
contract: &RemoteContract,
) -> Result<(), ApiError> {
let pool = api.state().db().pool();
if let Some(company) = &contract.company {
sqlx::query(
"INSERT INTO counterparty (id, name) VALUES (?1, ?2)
ON CONFLICT(id) DO UPDATE SET name = excluded.name",
)
.bind(company.id)
.bind(&company.name)
.execute(pool)
.await
.map_err(database_error)?;
}
if let Some(project) = &contract.project {
let (locality_id, locality_name) = match &project.locality {
Some(RemoteLocality::Object { id, name }) => (Some(*id), Some(name.as_str())),
Some(RemoteLocality::Id(id)) => (Some(*id), None),
None => (None, None),
};
sqlx::query(
"INSERT INTO project (id, name, full_name, short_name, locality_id, locality_name)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
full_name = excluded.full_name,
short_name = excluded.short_name,
locality_id = excluded.locality_id,
locality_name = excluded.locality_name",
)
.bind(project.id)
.bind(&project.name)
.bind(&project.full_name)
.bind(&project.short_name)
.bind(locality_id)
.bind(locality_name)
.execute(pool)
.await
.map_err(database_error)?;
}
if let Some(frc) = &contract.frc {
sqlx::query(
"INSERT INTO frc (id, name, icon, balance) VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
icon = excluded.icon,
balance = excluded.balance",
)
.bind(frc.id)
.bind(&frc.name)
.bind(frc.icon.as_deref().unwrap_or_default())
.bind(frc.balance.unwrap_or_default())
.execute(pool)
.await
.map_err(database_error)?;
}
if let Some(employee) = &contract.get_employee {
sqlx::query(
"INSERT INTO employee (id, name, short_name, avatar_small) VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
short_name = excluded.short_name,
avatar_small = excluded.avatar_small",
)
.bind(employee.id)
.bind(&employee.name)
.bind(&employee.short_name)
.bind(employee.avatar_small.as_deref())
.execute(pool)
.await
.map_err(database_error)?;
}
Ok(())
}
async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<(), ApiError> {
sqlx::query(
"INSERT INTO contractapp (
id, name, number, absolute_url, contract_type, category, amount_total_display,
amount_by_ds, comment, date, status_name, status, nds, name_of_product,
counterparty_name, amount, month_pay, avans_pay, amount_total, estimate_nds_cost,
estimate_nds_cert, bill_cost_sum, bill_paid_sum, income_total, arrears,
counterparty_id, project_id, frc_id, employee_id
)
VALUES (
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15,
?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29
)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
number = excluded.number,
absolute_url = excluded.absolute_url,
contract_type = excluded.contract_type,
category = excluded.category,
amount_total_display = excluded.amount_total_display,
amount_by_ds = excluded.amount_by_ds,
comment = excluded.comment,
date = excluded.date,
status_name = excluded.status_name,
status = excluded.status,
nds = excluded.nds,
name_of_product = excluded.name_of_product,
counterparty_name = excluded.counterparty_name,
amount = excluded.amount,
month_pay = excluded.month_pay,
avans_pay = excluded.avans_pay,
amount_total = excluded.amount_total,
estimate_nds_cost = excluded.estimate_nds_cost,
estimate_nds_cert = excluded.estimate_nds_cert,
bill_cost_sum = excluded.bill_cost_sum,
bill_paid_sum = excluded.bill_paid_sum,
income_total = excluded.income_total,
arrears = excluded.arrears,
counterparty_id = excluded.counterparty_id,
project_id = excluded.project_id,
frc_id = excluded.frc_id,
employee_id = excluded.employee_id",
)
.bind(contract.id)
.bind(&contract.name)
.bind(&contract.number)
.bind(&contract.absolute_url)
.bind(&contract.contract_type)
.bind(&contract.category)
.bind(&contract.amount_total_display)
.bind(&contract.amount_by_ds)
.bind(&contract.comment)
.bind(&contract.date)
.bind(&contract.status_name)
.bind(&contract.status)
.bind(&contract.nds)
.bind(&contract.name_of_product)
.bind(&contract.counterparty)
.bind(contract.amount)
.bind(contract.month_pay)
.bind(&contract.avans_pay)
.bind(contract.amount_total)
.bind(contract.estimate_nds_cost)
.bind(contract.estimate_nds_cert)
.bind(contract.bill_cost.as_ref().and_then(|bill_cost| bill_cost.cost_sum))
.bind(contract.bill_cost.as_ref().and_then(|bill_cost| bill_cost.paid_sum))
.bind(contract.income_total)
.bind(contract.arrears)
.bind(contract.company.as_ref().map(|company| company.id))
.bind(contract.project.as_ref().map(|project| project.id))
.bind(contract.frc.as_ref().map(|frc| frc.id))
.bind(contract.get_employee.as_ref().map(|employee| employee.id))
.execute(api.state().db().pool())
.await
.map_err(database_error)?;
Ok(())
}
fn database_error(error: sqlx::Error) -> ApiError {
ApiError::new("database_error", error.to_string())
}