            asof=data['as_of'];remaining={b['id']:b['qty'] for b in data['batches']};allocation=[];short=[]
            for order in data['orders']:
                need=order['qty']
                eligible=sorted((b for b in data['batches'] if b['sku']==order['sku'] and b['expires']>=asof),key=lambda b:(b['expires'],b['id']))
                for batch in eligible:
                    take=min(need,remaining[batch['id']])
                    if take:allocation.append({'order':order['id'],'batch':batch['id'],'quantity':take,'expires':batch['expires']});remaining[batch['id']]-=take;need-=take
                    if not need:break
                if need:short.append({'order':order['id'],'missing_quantity':need})
            check('过期批次B1没有出库',all(r['batch']!='B1' for r in allocation));check('先分配B2再B3',[r['batch'] for r in allocation]==['B2','B3','B3']);check('缺口两件保留',short==[{'order':'O2','missing_quantity':2}]);check('库存没有负数',all(x>=0 for x in remaining.values()));cout('batch-allocation.csv',allocation);cout('shortage.csv',short);result={'allocations':allocation,'shortages':short,'remaining':remaining}
