style(src-tauri): cargo fmt
Pure mechanical rustfmt reformat (default config, no rustfmt.toml).
This commit is contained in:
@@ -150,7 +150,12 @@ pub fn remarks_marker(number: &str) -> String {
|
||||
fn verified_totals(inv: &Invoice) -> Result<(TaxType, i64, Totals), String> {
|
||||
let tax_type = parse_tax_type(&inv.tax_type)?;
|
||||
let rate_bp = (inv.tax_rate * 100.0).round() as i64;
|
||||
let totals = gst::compute_totals(&inv.items, gst::rupees_to_paise(inv.discount), tax_type, rate_bp);
|
||||
let totals = gst::compute_totals(
|
||||
&inv.items,
|
||||
gst::rupees_to_paise(inv.discount),
|
||||
tax_type,
|
||||
rate_bp,
|
||||
);
|
||||
let stored = [
|
||||
(totals.cgst, inv.cgst_amount, "CGST"),
|
||||
(totals.sgst, inv.sgst_amount, "SGST"),
|
||||
@@ -199,13 +204,21 @@ fn distinct_descriptions(items: &[InvoiceItem]) -> Vec<String> {
|
||||
.enumerate()
|
||||
.map(|(i, item)| {
|
||||
let trimmed = item.description.trim();
|
||||
let full = if trimmed.is_empty() { format!("Item {}", i + 1) } else { trimmed.to_string() };
|
||||
let full = if trimmed.is_empty() {
|
||||
format!("Item {}", i + 1)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
};
|
||||
let (head, tail) = first_line_and_rest(&full);
|
||||
let mut candidate = full.clone();
|
||||
let mut n = 2;
|
||||
while used.contains(&candidate.to_lowercase()) {
|
||||
let head = format!("{head} ({n})");
|
||||
candidate = if tail.is_empty() { head } else { format!("{head}\n{tail}") };
|
||||
candidate = if tail.is_empty() {
|
||||
head
|
||||
} else {
|
||||
format!("{head}\n{tail}")
|
||||
};
|
||||
n += 1;
|
||||
}
|
||||
used.insert(candidate.to_lowercase());
|
||||
@@ -271,7 +284,10 @@ fn item_rows(ctx: &InvoiceContext, apply_gst: bool) -> Result<Vec<Value>, String
|
||||
));
|
||||
}
|
||||
let (head, _) = first_line_and_rest(&descriptions[i]);
|
||||
row.insert("item_name".into(), json!(truncate_chars(head, ITEM_NAME_MAX)));
|
||||
row.insert(
|
||||
"item_name".into(),
|
||||
json!(truncate_chars(head, ITEM_NAME_MAX)),
|
||||
);
|
||||
}
|
||||
}
|
||||
row.insert("description".into(), json!(descriptions[i]));
|
||||
@@ -296,7 +312,9 @@ fn item_rows(ctx: &InvoiceContext, apply_gst: bool) -> Result<Vec<Value>, String
|
||||
|
||||
fn tax_row(account: &str, head: &str, rate: &str) -> Result<Value, String> {
|
||||
if account.trim().is_empty() {
|
||||
return Err(format!("The {head} account is not set in the ERPNext settings."));
|
||||
return Err(format!(
|
||||
"The {head} account is not set in the ERPNext settings."
|
||||
));
|
||||
}
|
||||
Ok(json!({
|
||||
"charge_type": "On Net Total",
|
||||
@@ -306,10 +324,19 @@ fn tax_row(account: &str, head: &str, rate: &str) -> Result<Value, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn tax_rows(cfg: &ErpnextConfig, tax_type: TaxType, rate_bp: i64, supplier_state: &str) -> Result<Vec<Value>, String> {
|
||||
fn tax_rows(
|
||||
cfg: &ErpnextConfig,
|
||||
tax_type: TaxType,
|
||||
rate_bp: i64,
|
||||
supplier_state: &str,
|
||||
) -> Result<Vec<Value>, String> {
|
||||
match tax_type {
|
||||
TaxType::None => Ok(Vec::new()),
|
||||
TaxType::Igst => Ok(vec![tax_row(&cfg.igst_account, "IGST", &scaled_decimal(rate_bp, 2))?]),
|
||||
TaxType::Igst => Ok(vec![tax_row(
|
||||
&cfg.igst_account,
|
||||
"IGST",
|
||||
&scaled_decimal(rate_bp, 2),
|
||||
)?]),
|
||||
TaxType::CgstSgst => {
|
||||
// rate_bp is in hundredths of a percent; half of it in thousandths of a percent is rate_bp * 5.
|
||||
let half = scaled_decimal(rate_bp * 5, 3);
|
||||
@@ -319,7 +346,10 @@ fn tax_rows(cfg: &ErpnextConfig, tax_type: TaxType, rate_bp: i64, supplier_state
|
||||
} else {
|
||||
&cfg.sgst_account
|
||||
};
|
||||
Ok(vec![tax_row(&cfg.cgst_account, "CGST", &half)?, tax_row(second_account, second, &half)?])
|
||||
Ok(vec![
|
||||
tax_row(&cfg.cgst_account, "CGST", &half)?,
|
||||
tax_row(second_account, second, &half)?,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,7 +358,10 @@ pub fn build_sales_invoice(ctx: &InvoiceContext) -> Result<BuiltRequest, String>
|
||||
let inv = ctx.invoice;
|
||||
let cfg = ctx.config;
|
||||
if inv.status != "issued" {
|
||||
return Err(format!("Invoice {} is {}; only issued invoices are sent to ERPNext.", inv.number, inv.status));
|
||||
return Err(format!(
|
||||
"Invoice {} is {}; only issued invoices are sent to ERPNext.",
|
||||
inv.number, inv.status
|
||||
));
|
||||
}
|
||||
if ctx.customer.trim().is_empty() {
|
||||
return Err("The ERPNext customer is not resolved.".into());
|
||||
@@ -346,7 +379,9 @@ pub fn build_sales_invoice(ctx: &InvoiceContext) -> Result<BuiltRequest, String>
|
||||
}
|
||||
NamingMode::Series => {
|
||||
if cfg.naming_series.trim().is_empty() {
|
||||
return Err("ERPNext series naming is selected but no naming series is set.".into());
|
||||
return Err(
|
||||
"ERPNext series naming is selected but no naming series is set.".into(),
|
||||
);
|
||||
}
|
||||
body.insert("naming_series".into(), json!(cfg.naming_series.trim()));
|
||||
}
|
||||
@@ -396,14 +431,21 @@ pub fn build_sales_invoice(ctx: &InvoiceContext) -> Result<BuiltRequest, String>
|
||||
body.insert("discount_amount".into(), money(totals.discount));
|
||||
}
|
||||
body.insert("items".into(), Value::Array(item_rows(ctx, gst_fields)?));
|
||||
body.insert("taxes".into(), Value::Array(tax_rows(cfg, tax_type, rate_bp, &ctx.vendor.state_code)?));
|
||||
body.insert(
|
||||
"taxes".into(),
|
||||
Value::Array(tax_rows(cfg, tax_type, rate_bp, &ctx.vendor.state_code)?),
|
||||
);
|
||||
if ctx.submit {
|
||||
body.insert("docstatus".into(), json!(1));
|
||||
}
|
||||
|
||||
let mirror = cfg.naming_mode == NamingMode::Mirror;
|
||||
Ok(BuiltRequest {
|
||||
path: if mirror { SALES_INVOICE_V2 } else { SALES_INVOICE_V1 },
|
||||
path: if mirror {
|
||||
SALES_INVOICE_V2
|
||||
} else {
|
||||
SALES_INVOICE_V1
|
||||
},
|
||||
body: Value::Object(body),
|
||||
idempotent: mirror,
|
||||
})
|
||||
@@ -416,7 +458,11 @@ fn has_gstin(client: &Client) -> bool {
|
||||
!g.is_empty() && !g.eq_ignore_ascii_case("NA")
|
||||
}
|
||||
|
||||
pub fn build_customer(client: &Client, cfg: &ErpnextConfig, india_compliance: bool) -> Result<BuiltRequest, String> {
|
||||
pub fn build_customer(
|
||||
client: &Client,
|
||||
cfg: &ErpnextConfig,
|
||||
india_compliance: bool,
|
||||
) -> Result<BuiltRequest, String> {
|
||||
let name = client.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("The client has no name.".into());
|
||||
@@ -427,19 +473,33 @@ pub fn build_customer(client: &Client, cfg: &ErpnextConfig, india_compliance: bo
|
||||
set_if(&mut body, "customer_group", &cfg.customer_group);
|
||||
set_if(&mut body, "territory", &cfg.territory);
|
||||
if india_compliance {
|
||||
body.insert("gst_category".into(), json!(gst_category_label(&client.gst_category)?));
|
||||
let taxable_registration = matches!(client.gst_category.as_str(), "registered_regular" | "composition" | "sez");
|
||||
body.insert(
|
||||
"gst_category".into(),
|
||||
json!(gst_category_label(&client.gst_category)?),
|
||||
);
|
||||
let taxable_registration = matches!(
|
||||
client.gst_category.as_str(),
|
||||
"registered_regular" | "composition" | "sez"
|
||||
);
|
||||
if taxable_registration && has_gstin(client) {
|
||||
let gstin = client.gstin.trim().to_ascii_uppercase();
|
||||
gst::validate_gstin(&gstin).map_err(|e| format!("Client GSTIN: {e}"))?;
|
||||
body.insert("gstin".into(), json!(gstin));
|
||||
}
|
||||
}
|
||||
Ok(BuiltRequest { path: CUSTOMER, body: Value::Object(body), idempotent: false })
|
||||
Ok(BuiltRequest {
|
||||
path: CUSTOMER,
|
||||
body: Value::Object(body),
|
||||
idempotent: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// `customer` is the name ERPNext returned when the Customer was created (a duplicate becomes "X - 1").
|
||||
pub fn build_address(client: &Client, customer: &str, india_compliance: bool) -> Result<BuiltRequest, String> {
|
||||
pub fn build_address(
|
||||
client: &Client,
|
||||
customer: &str,
|
||||
india_compliance: bool,
|
||||
) -> Result<BuiltRequest, String> {
|
||||
if customer.trim().is_empty() {
|
||||
return Err("The ERPNext customer is not resolved.".into());
|
||||
}
|
||||
@@ -472,16 +532,31 @@ pub fn build_address(client: &Client, customer: &str, india_compliance: bool) ->
|
||||
set_if(&mut body, "pincode", &client.pincode);
|
||||
body.insert("country".into(), json!("India"));
|
||||
if india_compliance {
|
||||
body.insert("gst_category".into(), json!(gst_category_label(&client.gst_category)?));
|
||||
if has_gstin(client) && matches!(client.gst_category.as_str(), "registered_regular" | "composition" | "sez") {
|
||||
body.insert("gstin".into(), json!(client.gstin.trim().to_ascii_uppercase()));
|
||||
body.insert(
|
||||
"gst_category".into(),
|
||||
json!(gst_category_label(&client.gst_category)?),
|
||||
);
|
||||
if has_gstin(client)
|
||||
&& matches!(
|
||||
client.gst_category.as_str(),
|
||||
"registered_regular" | "composition" | "sez"
|
||||
)
|
||||
{
|
||||
body.insert(
|
||||
"gstin".into(),
|
||||
json!(client.gstin.trim().to_ascii_uppercase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
body.insert(
|
||||
"links".into(),
|
||||
json!([{ "link_doctype": "Customer", "link_name": customer.trim() }]),
|
||||
);
|
||||
Ok(BuiltRequest { path: ADDRESS, body: Value::Object(body), idempotent: false })
|
||||
Ok(BuiltRequest {
|
||||
path: ADDRESS,
|
||||
body: Value::Object(body),
|
||||
idempotent: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -539,7 +614,13 @@ mod tests {
|
||||
Vendor::from_snapshot(&inv.vendor_snapshot).unwrap()
|
||||
}
|
||||
|
||||
fn build(inv: &Invoice, cfg: &ErpnextConfig, ic: bool, codes: &[Option<String>], submit: bool) -> Result<BuiltRequest, String> {
|
||||
fn build(
|
||||
inv: &Invoice,
|
||||
cfg: &ErpnextConfig,
|
||||
ic: bool,
|
||||
codes: &[Option<String>],
|
||||
submit: bool,
|
||||
) -> Result<BuiltRequest, String> {
|
||||
let v = vendor(inv);
|
||||
build_sales_invoice(&InvoiceContext {
|
||||
invoice: inv,
|
||||
@@ -597,7 +678,10 @@ mod tests {
|
||||
assert_eq!(scaled_decimal(1500, 3), "1.5");
|
||||
assert_eq!(scaled_decimal(2625, 3), "2.625");
|
||||
assert_eq!(money(731_005).to_string(), "7310.05");
|
||||
assert_eq!(money(i64::from(u32::MAX) * 100 + 7).to_string(), "4294967295.07");
|
||||
assert_eq!(
|
||||
money(i64::from(u32::MAX) * 100 + 7).to_string(),
|
||||
"4294967295.07"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -607,7 +691,10 @@ mod tests {
|
||||
assert_eq!(place_of_supply_label("97").unwrap(), "97-Other Territory");
|
||||
assert_eq!(place_of_supply_label("96").unwrap(), "96-Other Countries");
|
||||
for (code, name) in gst::STATES {
|
||||
assert_eq!(place_of_supply_label(code).unwrap(), format!("{code}-{name}"));
|
||||
assert_eq!(
|
||||
place_of_supply_label(code).unwrap(),
|
||||
format!("{code}-{name}")
|
||||
);
|
||||
}
|
||||
assert!(place_of_supply_label("28").is_err());
|
||||
}
|
||||
@@ -750,10 +837,16 @@ mod tests {
|
||||
#[test]
|
||||
fn plain_erpnext_registered_vendor_gets_tax_rows_but_no_india_compliance_fields() {
|
||||
let mut conn = registered();
|
||||
let inv = issue(&mut conn, json!({ "items": [{"description": "Design", "mode": "fixed", "amount": 100.0, "hsnSac": "998314"}] }));
|
||||
let inv = issue(
|
||||
&mut conn,
|
||||
json!({ "items": [{"description": "Design", "mode": "fixed", "amount": 100.0, "hsnSac": "998314"}] }),
|
||||
);
|
||||
let req = build(&inv, &cfg(), false, &[None], false).unwrap();
|
||||
assert_eq!(req.body["taxes"].as_array().unwrap().len(), 2);
|
||||
assert!(req.body.get("place_of_supply").is_none() && req.body.get("is_reverse_charge").is_none());
|
||||
assert!(
|
||||
req.body.get("place_of_supply").is_none()
|
||||
&& req.body.get("is_reverse_charge").is_none()
|
||||
);
|
||||
assert!(req.body["items"][0].get("gst_hsn_code").is_none());
|
||||
}
|
||||
|
||||
@@ -795,8 +888,17 @@ mod tests {
|
||||
let req = build(&inv, &cfg(), false, &codes, false).unwrap();
|
||||
let rows = req.body["items"].as_array().unwrap();
|
||||
assert!(rows[0].get("stock_uom").is_none());
|
||||
assert_eq!((rows[0]["uom"].as_str(), rows[0]["conversion_factor"].as_i64()), (Some("Hour"), Some(1)));
|
||||
assert_eq!((rows[1]["uom"].as_str(), rows[1]["stock_uom"].as_str()), (Some("Minute"), Some("Minute")));
|
||||
assert_eq!(
|
||||
(
|
||||
rows[0]["uom"].as_str(),
|
||||
rows[0]["conversion_factor"].as_i64()
|
||||
),
|
||||
(Some("Hour"), Some(1))
|
||||
);
|
||||
assert_eq!(
|
||||
(rows[1]["uom"].as_str(), rows[1]["stock_uom"].as_str()),
|
||||
(Some("Minute"), Some("Minute"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -849,7 +951,10 @@ mod tests {
|
||||
assert!(!req.idempotent);
|
||||
assert!(req.body.get("name").is_none());
|
||||
assert_eq!(req.body["naming_series"], "SINV-.YY.-");
|
||||
assert_eq!(req.body["remarks"], format!("Voiced invoice {}\nNet 30", inv.number));
|
||||
assert_eq!(
|
||||
req.body["remarks"],
|
||||
format!("Voiced invoice {}\nNet 30", inv.number)
|
||||
);
|
||||
|
||||
c.naming_series.clear();
|
||||
assert!(build(&inv, &c, true, &[None], false).is_err());
|
||||
@@ -859,8 +964,15 @@ mod tests {
|
||||
fn submit_adds_docstatus_and_drafts_do_not() {
|
||||
let mut conn = registered();
|
||||
let inv = issue(&mut conn, json!({}));
|
||||
assert!(build(&inv, &cfg(), true, &[None], false).unwrap().body.get("docstatus").is_none());
|
||||
assert_eq!(build(&inv, &cfg(), true, &[None], true).unwrap().body["docstatus"], 1);
|
||||
assert!(build(&inv, &cfg(), true, &[None], false)
|
||||
.unwrap()
|
||||
.body
|
||||
.get("docstatus")
|
||||
.is_none());
|
||||
assert_eq!(
|
||||
build(&inv, &cfg(), true, &[None], true).unwrap().body["docstatus"],
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -874,7 +986,9 @@ mod tests {
|
||||
|
||||
// A supplier in a union territory without a legislature charges UTGST, not SGST.
|
||||
let mut ut = inv.clone();
|
||||
ut.vendor_snapshot = ut.vendor_snapshot.replace("\"vendorStateCode\":\"27\"", "\"vendorStateCode\":\"04\"");
|
||||
ut.vendor_snapshot = ut
|
||||
.vendor_snapshot
|
||||
.replace("\"vendorStateCode\":\"27\"", "\"vendorStateCode\":\"04\"");
|
||||
let req = build(&ut, &cfg(), true, &[None], false).unwrap();
|
||||
assert_eq!(req.body["taxes"][1]["account_head"], "Output UTGST - AC");
|
||||
assert_eq!(req.body["taxes"][1]["description"], "UTGST @ 2.625%");
|
||||
@@ -901,11 +1015,17 @@ mod tests {
|
||||
let mut inv = issue(&mut conn, json!({}));
|
||||
let mut c = cfg();
|
||||
c.cgst_account.clear();
|
||||
assert!(build(&inv, &c, true, &[None], false).unwrap_err().contains("CGST account"));
|
||||
assert!(build(&inv, &c, true, &[None], false)
|
||||
.unwrap_err()
|
||||
.contains("CGST account"));
|
||||
c.company.clear();
|
||||
assert!(build(&inv, &c, true, &[None], false).unwrap_err().contains("company"));
|
||||
assert!(build(&inv, &c, true, &[None], false)
|
||||
.unwrap_err()
|
||||
.contains("company"));
|
||||
inv.status = "cancelled".into();
|
||||
assert!(build(&inv, &cfg(), true, &[None], false).unwrap_err().contains("only issued"));
|
||||
assert!(build(&inv, &cfg(), true, &[None], false)
|
||||
.unwrap_err()
|
||||
.contains("only issued"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -913,9 +1033,17 @@ mod tests {
|
||||
let mut conn = registered();
|
||||
let mut inv = issue(&mut conn, json!({}));
|
||||
inv.due_date = "2026-03-01".into();
|
||||
assert!(build(&inv, &cfg(), true, &[None], false).unwrap().body.get("due_date").is_none());
|
||||
assert!(build(&inv, &cfg(), true, &[None], false)
|
||||
.unwrap()
|
||||
.body
|
||||
.get("due_date")
|
||||
.is_none());
|
||||
inv.due_date.clear();
|
||||
assert!(build(&inv, &cfg(), true, &[None], false).unwrap().body.get("due_date").is_none());
|
||||
assert!(build(&inv, &cfg(), true, &[None], false)
|
||||
.unwrap()
|
||||
.body
|
||||
.get("due_date")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -960,11 +1088,17 @@ mod tests {
|
||||
}
|
||||
let mut unreg = client();
|
||||
unreg.gst_category = "unregistered".into();
|
||||
assert!(build_customer(&unreg, &cfg(), true).unwrap().body.get("gstin").is_none());
|
||||
assert!(build_customer(&unreg, &cfg(), true)
|
||||
.unwrap()
|
||||
.body
|
||||
.get("gstin")
|
||||
.is_none());
|
||||
|
||||
let mut bad = client();
|
||||
bad.gstin = "29AABCU9603R1Z0".into();
|
||||
assert!(build_customer(&bad, &cfg(), true).unwrap_err().contains("GSTIN"));
|
||||
assert!(build_customer(&bad, &cfg(), true)
|
||||
.unwrap_err()
|
||||
.contains("GSTIN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -989,7 +1123,10 @@ mod tests {
|
||||
);
|
||||
let mut jk = client();
|
||||
jk.state_code = "01".into();
|
||||
assert_eq!(build_address(&jk, "X", true).unwrap().body["state"], "Jammu and Kashmir");
|
||||
assert_eq!(
|
||||
build_address(&jk, "X", true).unwrap().body["state"],
|
||||
"Jammu and Kashmir"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -997,7 +1134,10 @@ mod tests {
|
||||
let mut c = client();
|
||||
c.address_line1.clear();
|
||||
c.address = "5 Park Street\nKolkata, West Bengal - 700016".into();
|
||||
assert_eq!(build_address(&c, "X", false).unwrap().body["address_line1"], "5 Park Street");
|
||||
assert_eq!(
|
||||
build_address(&c, "X", false).unwrap().body["address_line1"],
|
||||
"5 Park Street"
|
||||
);
|
||||
|
||||
let mut no_city = client();
|
||||
no_city.city.clear();
|
||||
@@ -1005,7 +1145,11 @@ mod tests {
|
||||
let mut no_state = client();
|
||||
no_state.state_code.clear();
|
||||
assert!(build_address(&no_state, "X", true).is_err());
|
||||
assert!(build_address(&no_state, "X", false).unwrap().body.get("state").is_none());
|
||||
assert!(build_address(&no_state, "X", false)
|
||||
.unwrap()
|
||||
.body
|
||||
.get("state")
|
||||
.is_none());
|
||||
assert!(build_address(&client(), " ", true).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user