Codeium Golfide

C, 208 205 175 169 bytes

argv[1] : cation
argv[2] : anion
Charges of ions are taken on stdin.

#define z(b)for(i=*++v;*++i>95;);printf(b>1?*i?"(%s)_%d":"%s_%d":"%s",*v,b);
main(c,v,d,e,g,h,i)char**v,*i;{scanf("%d%d",&c,&d);for(e=c,h=-d;g=h;e=g)h=e%g;z(-d/e)z(c/e)}

Retina, 86 80 bytes

Thanks to Neil for saving 6 bytes.

^
(
\^.+
)_$&$*
(1+)(\1|¶.+)+_(\1)+$
$#3$2_$#2
_1$

m)T`()``.+\)$|\(.[a-z]?\)
¶

Try it online!

Input is linefeed-separated (test suite uses comma-separation for convenience).

Explanation

^
(

We start by prepending a ( to each molecule. The ^ matches on line beginnings because the m) towards the end of the program sets multiline mode for all preceding stages.

\^.+
)_$&$*

We replace the ^[-+]n part with )_, followed by n copies of 1 (i.e. we convert the charges to unary, dropping the signs).

(1+)(\1|¶.+)+_(\1)+$
$#3$2_$#2

This stage does three things: it divides both charges by their GCD, converts them back to decimal and swaps them. The GCD can be found quite easily in regex, by matching the longest 1+ that lets us match both charges using only the backreference \1. To divide by this, we make use of Retina's "capture count" feature, which tells us how often a group has been used. So $#2 is the first charge divided by the GCD and $#3 is the second charge divided by the GCD (both in decimal).

_1$

We remove _1s from the ends of both parts.

m)T`()``.+\)$|\(.[a-z]?\)

And we drop the parentheses from lines which end in a ) (i.e. those that had a _1 there), as well as lines that only contain a single atom.

Finally, we concatenate the two molecules by dropping the linefeed.


APL (Dyalog), 60 59 61 bytes

+2 since charges must be given signed.

Anonymous infix function. Takes list of ions (anion, cation) as left argument and list of corresponding charges as right argument.

{∊(⍺{⍵∧1<≢⍺∩⎕D,⎕A:1⌽')(',⍺⋄⍺}¨m),¨(m←s≠1)/¨'_',∘⍕¨s←⍵÷⍨∧/⍵}∘|

Try it online!

{}∘| function where is left argument and is right argument's magnitude:

∧/⍵ LCM of the charges

⍵÷⍨ divide the charges by that

s← store in s (for subscripts)

'_',∘⍕¨ format (stringify) and prepend underbar to each

()/ replicate each letter of each with the corresponding value from:

  s≠1 Is s different from 1? (gives 1 or 0)

  m← store in m (for multiple)

(),¨ prepend the following respectively to those:

  ⍺{}¨m for each, call this function with ions and m as arguments:

   ⎕D,⎕ADigits followed by uppercase Alphabet

   ⍺∩ intersection of ion and that

    tally the number of characters in that

   1< Is one less than that? (i.e. do we have a multi-element ion?)

   ⍵∧ and do we need multiple of that ion?

   : if so, then:

    ')(',⍺ prepend the string to the ion

    1⌽ cyclically rotate one step to the left (puts ) on the right)

    else

     return the ion unmodified

ϵnlist (flatten)