I see two ways to go about it, both quite similar.
A) You can collect IDs of displayed posts in an array and then exclude those with post__not_in
query parameter like so:
$years_loop = get_terms(
array(
'taxonomy' => 'works_year',
'orderby' => 'slug',
'order' => 'DESC',
)
);
// array for displayed posts, we will update it with every loop
$displayed = [];
foreach($years_loop as $year_loop_slug) :
$args = array(
'post_type' => 'works',
'posts_per_page' => -1,
'post__not_in' => array_unique( $displayed ),
'tax_query' => array(
array(
'taxonomy' => 'works_year',
'field' => 'slug',
'terms' => array( $year_loop_slug->slug ),
'operator' => 'IN',
)
),
);
$loop = new WP_Query($args);
while ($loop->have_posts()) : $loop->the_post();
// update array with currently displayed post ID
$displayed[] = get_the_ID();
//your output
endwhile;
endforeach;
B) If you want to avoid using post__not_in
parameter, which can have bad impact on your site's performance, you can check for the duplicates inside your loops:
$years_loop = get_terms(
array(
'taxonomy' => 'works_year',
'orderby' => 'slug',
'order' => 'DESC',
)
);
// array for displayed posts, we will update it with every loop
$displayed = [];
foreach($years_loop as $year_loop_slug) :
$args = array(
'post_type' => 'works',
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => 'works_year',
'field' => 'slug',
'terms' => array( $year_loop_slug->slug ),
'operator' => 'IN',
)
),
);
$loop = new WP_Query($args);
while ($loop->have_posts()) : $loop->the_post();
// if the current post was already displayed, move on to the next iteration of this loop
if ( in_array( get_the_ID(), $displayed ) ){
continue;
}
// update array with currently displayed post ID
$displayed[] = get_the_ID();
//your output
endwhile;
endforeach;
Code not tested, but you get the idea.