models.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. from django.db import models
  2. from erp.models import InboundBill, MaterialDetail, OutboundBill,OutMaterialDetail
  3. from django.db.models import Sum
  4. from django.db.models.signals import post_save, post_delete, pre_save
  5. from django.dispatch import receiver
  6. from decimal import Decimal
  7. class BoundListModel(models.Model):
  8. STATUS =(
  9. ("100", '入库申请'), ("101", '入库同意'), ("102", '组盘中'), ("103", '部分入库'), ("104", '已入库'),
  10. ("111", '质检合格'),
  11. ("200", '出库申请'), ("201", '出库同意'), ("202", '出库中'), ("203", '部分出库'), ("204", '已出库'),
  12. ("300", '完成')
  13. )
  14. audit_status = models.BooleanField(default=False, verbose_name='审核状态')
  15. bound_month = models.CharField(max_length=255, verbose_name="月份")
  16. bound_date = models.DateField(verbose_name="单据日期")
  17. bound_code = models.CharField(max_length=255, verbose_name="出入库编号",unique=True)
  18. bound_code_type = models.CharField(max_length=255, verbose_name="单据类型")
  19. bound_bs_type = models.CharField(max_length=255, verbose_name="业务类型")
  20. bound_type = models.CharField(max_length=255, verbose_name="出入库类型")
  21. bound_desc = models.CharField(default='', max_length=255, verbose_name="出入库描述")
  22. bound_department = models.CharField(max_length=255, verbose_name="部门")
  23. base_type = models.IntegerField(default=0, verbose_name="基准类型")
  24. bound_status = models.CharField(max_length=255, verbose_name="单据状态",choices=STATUS, default='100')
  25. note = models.CharField(default='', max_length=255, verbose_name="备注")
  26. creater = models.CharField(default='uesr', max_length=255, verbose_name="Who Created")
  27. openid = models.CharField(max_length=255, verbose_name="Openid")
  28. is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
  29. create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
  30. update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
  31. relate_bill = models.ForeignKey(InboundBill, on_delete=models.CASCADE, verbose_name="关联单据", related_name='bound_list',null=True, blank=True)
  32. relate_out_bill = models.ForeignKey(OutboundBill, on_delete=models.CASCADE, verbose_name="关联出库单据", related_name='bound_out_list',null=True, blank=True)
  33. class Meta:
  34. db_table = 'boundlist'
  35. verbose_name = 'Bound List'
  36. verbose_name_plural = "Bound List"
  37. ordering = ['-id']
  38. class BoundBatchModel(models.Model):
  39. CONTAINER_STATUS = (
  40. (0, '入库申请'),
  41. (1, '入库中'),
  42. (2, '部分入库'),
  43. (3, '在库'),
  44. (4, '出库中'),
  45. (5, '部分出库'),
  46. (6, '已出库'),
  47. )
  48. CHECK_STATUS=(
  49. (0, '未质检'),
  50. (1, '已质检'),
  51. (2, '质检不合格'),
  52. )
  53. bound_number = models.CharField(max_length=255, verbose_name="入库批次号",blank=False, null=False, unique=True)
  54. sourced_number = models.CharField(max_length=255, verbose_name="来源批次号",blank=True, null=True)
  55. bound_month = models.CharField(max_length=255, verbose_name="月份")
  56. bound_batch_order = models.IntegerField(default=0, verbose_name="批次顺序")
  57. warehouse_code = models.CharField(max_length=255, verbose_name="仓库编码")
  58. warehouse_name = models.CharField(max_length=255, verbose_name="仓库名称")
  59. goods_code = models.CharField(max_length=255, verbose_name="商品编码")
  60. goods_desc = models.CharField(max_length=255, verbose_name="商品描述")
  61. goods_std = models.CharField(default='待填写', max_length=255, verbose_name="商品标准",blank=True, null=True)
  62. goods_unit = models.CharField(default='待填写', max_length=255, verbose_name="商品单位")
  63. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="商品数量")
  64. goods_package = models.CharField(default='待填写', max_length=255, verbose_name="商品包装")
  65. goods_in_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="组盘入库数量")
  66. goods_in_location_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="库位入库数量")
  67. goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="出库数量")
  68. goods_reserve_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="预定出库数量")
  69. status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='批次状态')
  70. container_number = models.IntegerField( default=0, verbose_name="托盘数目")
  71. goods_weight = models.FloatField(default=0, verbose_name="商品单重")
  72. goods_total_weight = models.FloatField(default=0, verbose_name="商品总重")
  73. note = models.CharField(default='', max_length=255, verbose_name="备注")
  74. creater = models.CharField(default='uesr', max_length=255, verbose_name="Who Created")
  75. openid = models.CharField(max_length=255, verbose_name="Openid")
  76. is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
  77. create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
  78. update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
  79. relate_material = models.ForeignKey(MaterialDetail, on_delete=models.CASCADE, verbose_name="关联物料", related_name='bound_batch',null=True, blank=True)
  80. check_status = models.IntegerField(choices=CHECK_STATUS, default=0, verbose_name='质检状态')
  81. check_user = models.CharField(default='', max_length=255, verbose_name="质检人")
  82. check_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="质检时间")
  83. class Meta:
  84. db_table = 'boundbatch'
  85. verbose_name = 'Bound Batch'
  86. verbose_name_plural = "Bound Batch"
  87. ordering = ['-id']
  88. def __str__(self):
  89. return f"{self.bound_number} - {self.goods_code} - {self.goods_desc}"
  90. class OutBoundDemandModel(models.Model):
  91. OUT_TYPE = (
  92. (0, '发货出库'),
  93. (4, '其他出库'),
  94. )
  95. bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name="Bound List", related_name='out_demand')
  96. goods_code = models.CharField(max_length=255, verbose_name="商品编码")
  97. goods_desc = models.CharField(max_length=255, verbose_name="商品描述")
  98. goods_std = models.CharField(default='待填写', max_length=255, verbose_name="商品标准",blank=True, null=True)
  99. goods_unit = models.CharField(default='待填写', max_length=255, verbose_name="商品单位")
  100. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="计划数量")
  101. working = models.BooleanField(default=False, verbose_name="是否在工作")
  102. creater = models.CharField(default='uesr', max_length=255, verbose_name="Who Created")
  103. create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
  104. update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
  105. is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
  106. out_type = models.IntegerField(choices=OUT_TYPE, default=4, verbose_name="出库类型")
  107. class Meta:
  108. db_table = 'outbounddemand'
  109. verbose_name = '出库需求'
  110. verbose_name_plural = "出库需求"
  111. ordering = ['-id']
  112. class MaterialStatistics(models.Model):
  113. goods_code = models.CharField(max_length=255, verbose_name="商品编码", unique=True)
  114. goods_desc = models.CharField(max_length=255, verbose_name="商品描述")
  115. goods_std = models.CharField(default='待填写', max_length=255, verbose_name="商品标准", blank=True, null=True)
  116. goods_unit = models.CharField(default='待填写', max_length=255, verbose_name="商品单位")
  117. total_quantity = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="在库数量")
  118. total_demanded_quantity = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="需求数量")
  119. def __str__(self):
  120. return f"{self.goods_code} - {self.goods_desc}"
  121. @property
  122. def bound_batches(self):
  123. return BoundBatchModel.objects.filter(goods_code=self.goods_code).exclude(goods_in_location_qty=0).order_by('bound_batch_order')
  124. @classmethod
  125. def get_period_summary(cls, goods_code, start_date, end_date):
  126. """获取指定时间段的库存变动汇总"""
  127. period_data = cls.objects.filter(
  128. goods_code=goods_code,
  129. history_records__change_time__range=(start_date, end_date)
  130. ).annotate(
  131. period_in=Sum('history_records__in_quantity'),
  132. period_out=Sum('history_records__out_quantity'),
  133. ).first()
  134. if period_data:
  135. return {
  136. 'period_in': period_data.period_in or Decimal('0'),
  137. 'period_out': period_data.period_out or Decimal('0')
  138. }
  139. return {'period_in': Decimal('0'), 'period_out': Decimal('0')}
  140. class Meta:
  141. db_table = 'materialstatistics'
  142. verbose_name = '物料统计'
  143. verbose_name_plural = "物料统计"
  144. ordering = ['goods_code']
  145. @receiver(pre_save, sender=BoundBatchModel)
  146. def store_original_goods_code(sender, instance, **kwargs):
  147. """保存前记录原始物料编码(仅更新时生效)"""
  148. if not instance.pk: # 新建对象无原始值
  149. return
  150. try:
  151. original = BoundBatchModel.objects.get(pk=instance.pk)
  152. # 将原始值存储在实例属性中供post_save使用
  153. instance._original_goods_code = original.goods_code
  154. except BoundBatchModel.DoesNotExist:
  155. pass
  156. @receiver([post_save, post_delete], sender=BoundBatchModel)
  157. def update_material_statistics(sender, instance, **kwargs):
  158. # 处理删除/创建/更新操作的核心逻辑
  159. def refresh_stats(target_goods_code, force_desc=False):
  160. """刷新指定物料的统计数据"""
  161. stats, created = MaterialStatistics.objects.get_or_create(
  162. goods_code=target_goods_code,
  163. defaults={
  164. 'goods_desc': instance.goods_desc,
  165. 'goods_std': instance.goods_std or '待填写',
  166. 'goods_unit': instance.goods_unit or '待填写',
  167. }
  168. )
  169. # 仅强制更新描述(用于当前物料)或新建时初始化
  170. if force_desc or created:
  171. stats.goods_desc = instance.goods_desc
  172. if instance.goods_std and instance.goods_std != '待填写':
  173. stats.goods_std = instance.goods_std
  174. if instance.goods_unit and instance.goods_unit != '待填写':
  175. stats.goods_unit = instance.goods_unit
  176. # 重新聚合统计数据(始终执行)
  177. agg = BoundBatchModel.objects.filter(goods_code=target_goods_code).aggregate(
  178. total=Sum('goods_in_location_qty'),
  179. total_out=Sum('goods_out_qty')
  180. )
  181. stats.total_quantity = agg['total'] or 0
  182. # 如果有出库统计字段需更新,可在此添加
  183. stats.save()
  184. # 情况1:删除操作直接更新当前物料
  185. if kwargs.get('signal') == post_delete:
  186. refresh_stats(instance.goods_code)
  187. return
  188. # 情况2:创建操作直接更新新物料
  189. if kwargs.get('created'):
  190. refresh_stats(instance.goods_code, force_desc=True)
  191. return
  192. # 情况3:更新操作(检测物料编码变更)
  193. original_code = getattr(instance, '_original_goods_code', None)
  194. current_code = instance.goods_code
  195. # 物料编码未变化时仅更新当前物料
  196. if original_code == current_code:
  197. refresh_stats(current_code, force_desc=True)
  198. # 物料编码变化时更新新旧两个物料
  199. else:
  200. refresh_stats(original_code) # 更新旧物料(仅刷新数量)
  201. refresh_stats(current_code, force_desc=True) # 更新新物料(全量刷新)
  202. # 物料统计的扩展方法
  203. def get_material_period_report(goods_code, start_date, end_date):
  204. """获取物料在特定时间段的变动报告"""
  205. stats = MaterialStatistics.objects.filter(goods_code=goods_code).first()
  206. if not stats:
  207. return None
  208. # 获取时间段的变动汇总
  209. period_summary = stats.get_period_summary(start_date, end_date)
  210. from container.models import MaterialChangeHistory
  211. # 计算期初数量(时间段开始前的最新库存)
  212. opening_record = MaterialChangeHistory.objects.filter(
  213. material_stats=stats,
  214. change_time__lt=start_date
  215. ).order_by('-change_time').first()
  216. opening_quantity = opening_record.closing_quantity if opening_record else Decimal('0')
  217. # 计算期末数量
  218. period_net = period_summary['period_in'] - period_summary['period_out']
  219. closing_quantity = opening_quantity + period_net
  220. return {
  221. 'goods_code': goods_code,
  222. 'goods_desc': stats.goods_desc,
  223. 'opening_quantity': opening_quantity,
  224. 'period_in': period_summary['period_in'],
  225. 'period_out': period_summary['period_out'],
  226. 'net_change': period_net,
  227. 'closing_quantity': closing_quantity,
  228. 'current_quantity': stats.total_quantity
  229. }
  230. class OutBatchModel(models.Model):
  231. CONTAINER_STATUS = (
  232. (0, '申请'),
  233. (1, '在途'),
  234. (2, '已出库')
  235. )
  236. OUT_TYPE = (
  237. (0, '发货出库'),
  238. (4, '其他出库'),
  239. )
  240. bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name="Bound List", related_name='out_batch_list',blank=True, null=True)
  241. out_number = models.CharField(max_length=255, verbose_name="出库批次号",blank=False, null=False)
  242. batch_number = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name="批次号", related_name='out_batch')
  243. out_date = models.DateTimeField(verbose_name="出库日期")
  244. out_type = models.IntegerField(choices=OUT_TYPE, default=4, verbose_name="出库类型")
  245. out_note = models.CharField(default='', max_length=255, verbose_name="出库备注")
  246. warehouse_code = models.CharField(max_length=255, verbose_name="仓库编码")
  247. warehouse_name = models.CharField(max_length=255, verbose_name="仓库名称")
  248. goods_code = models.CharField(max_length=255, verbose_name="商品编码")
  249. goods_desc = models.CharField(max_length=255, verbose_name="商品描述")
  250. goods_std = models.CharField(default='待填写', max_length=255, verbose_name="商品标准",blank=True, null=True)
  251. goods_unit = models.CharField(default='待填写', max_length=255, verbose_name="商品单位")
  252. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="商品数量")
  253. goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="出库数量")
  254. status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='批次状态')
  255. container_number = models.IntegerField( default=0, verbose_name="托盘数目")
  256. goods_weight = models.FloatField(default=0, verbose_name="商品单重")
  257. goods_total_weight = models.FloatField(default=0, verbose_name="商品总重")
  258. creater = models.CharField(default='uesr', max_length=255, verbose_name="Who Created")
  259. openid = models.CharField(max_length=255, verbose_name="Openid")
  260. is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
  261. create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
  262. update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
  263. relate_material = models.ForeignKey(OutMaterialDetail, on_delete=models.CASCADE, verbose_name="关联物料", related_name='out_batch',null=True, blank=True)
  264. class Meta:
  265. db_table = 'outbatch'
  266. verbose_name = 'Out Batch'
  267. verbose_name_plural = "Out Batch"
  268. ordering = ['-id']
  269. @receiver([post_save, post_delete], sender=OutBatchModel)
  270. def update_material_demanded(sender, instance, **kwargs):
  271. goods_code = instance.goods_code
  272. stats, created = MaterialStatistics.objects.get_or_create(
  273. goods_code=goods_code,
  274. defaults={
  275. 'goods_desc': instance.goods_desc,
  276. 'goods_std': instance.goods_std or '待填写',
  277. 'goods_unit': instance.goods_unit or '待填写',
  278. }
  279. )
  280. # 更新物料信息为最新批次的信息(可选)
  281. stats.goods_desc = instance.goods_desc
  282. if instance.goods_std and instance.goods_std != '待填写':
  283. stats.goods_std = instance.goods_std
  284. if instance.goods_unit and instance.goods_unit != '待填写':
  285. stats.goods_unit = instance.goods_unit
  286. stats.save()
  287. # 计算总数量
  288. total = OutBatchModel.objects.filter(goods_code=goods_code,is_delete=False).aggregate(
  289. total=Sum('goods_out_qty')
  290. )['total'] or 0
  291. stats.total_demanded_quantity = total
  292. stats.save()
  293. batch_number = instance.batch_number
  294. batch_out_qty = OutBatchModel.objects.filter(batch_number=batch_number,goods_code=goods_code,is_delete=False).aggregate(
  295. total=Sum('goods_out_qty')
  296. )['total'] or 0
  297. batch_number.goods_reserve_qty = batch_out_qty
  298. batch_number.save()
  299. # 利用创建好的批次来与申请单相对应
  300. class OutBoundDetailModel(models.Model):
  301. CONTAINER_STATUS = (
  302. (0, '出库申请'),
  303. (1, '出库中'),
  304. (2, '已出库'),
  305. (3, '入库中'),
  306. (4, '已入库')
  307. )
  308. bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name="Bound List", related_name='out_bound_detail')
  309. bound_batch = models.ForeignKey(OutBatchModel, on_delete=models.PROTECT, verbose_name="Bound Batch", related_name='out_bound_batch_detail')
  310. bound_batch_number = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name="Bound Batch Number", related_name='out_bound_batch_number_detail')
  311. status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='批次状态')
  312. detail_code = models.CharField(max_length=255, verbose_name="明细编号")
  313. creater = models.CharField(default='uesr', max_length=255, verbose_name="Who Created")
  314. openid = models.CharField(max_length=255, verbose_name="Openid")
  315. is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
  316. create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
  317. update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
  318. class Meta:
  319. db_table = 'Outbounddetail'
  320. verbose_name = 'OutBound Detail'
  321. verbose_name_plural = "OutBound Detail"
  322. ordering = ['-id']
  323. class BatchOperateLogModel(models.Model):
  324. BATCH_LOG_TYPE = (
  325. (0, '入库'),
  326. (1, '出库'),
  327. (2, '移库'),
  328. (9, '其他'),
  329. )
  330. batch_id =models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name="批次ID", related_name='batch_log')
  331. log_type = models.IntegerField(choices=BATCH_LOG_TYPE, default=9, verbose_name="日志类型")
  332. log_date = models.DateTimeField(verbose_name="日志日期")
  333. goods_code = models.CharField(max_length=255, verbose_name="商品编码")
  334. goods_desc = models.CharField(max_length=255, verbose_name="商品描述")
  335. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name="商品数量")
  336. log_content = models.CharField(max_length=255, verbose_name="日志内容")
  337. creater = models.CharField(default='uesr', max_length=255, verbose_name="Who Created")
  338. openid = models.CharField(max_length=255, verbose_name="Openid")
  339. is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
  340. create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
  341. update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
  342. class Meta:
  343. db_table = 'batchlog'
  344. verbose_name = 'Batch Log'
  345. verbose_name_plural = "Batch Log"
  346. # 利用创建好的批次来与申请单相对应
  347. class BoundDetailModel(models.Model):
  348. CONTAINER_STATUS = (
  349. (0, '入库申请'),
  350. (1, '入库中'),
  351. (2, '在库'),
  352. (3, '出库中'),
  353. (4, '已出库')
  354. )
  355. bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name="Bound List", related_name='bound_detail')
  356. bound_batch = models.ForeignKey(BoundBatchModel, on_delete=models.PROTECT, verbose_name="Bound Batch", related_name='bound_batch_detail')
  357. status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='批次状态')
  358. detail_code = models.CharField(max_length=255, verbose_name="明细编号",unique=True)
  359. creater = models.CharField(default='uesr', max_length=255, verbose_name="Who Created")
  360. openid = models.CharField(max_length=255, verbose_name="Openid")
  361. is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
  362. create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
  363. update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
  364. class Meta:
  365. db_table = 'bounddetail'
  366. verbose_name = 'Bound Detail'
  367. verbose_name_plural = "Bound Detail"
  368. ordering = ['-id']