GoodERP
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

292 line
13KB

  1. # -*- coding: utf-8 -*-
  2. from .utils import safe_division
  3. from odoo.exceptions import UserError
  4. from odoo import models, fields, api
  5. from odoo import _
  6. from odoo.tools import float_compare
  7. import logging
  8. _logger = logging.getLogger(__name__)
  9. class Goods(models.Model):
  10. _inherit = 'goods'
  11. net_weight = fields.Float('净重', digits='Weight')
  12. current_qty = fields.Float('当前数量', compute='compute_stock_qty', digits='Quantity')
  13. max_stock_qty = fields.Float('库存上限', digits='Quantity')
  14. min_stock_qty = fields.Float('库存下限', digits='Quantity')
  15. moq = fields.Float('最小订单量', digits='Quantity')
  16. sell_lead_time = fields.Char('销售备货周期')
  17. excess = fields.Boolean('允许订单超发')
  18. bom_count = fields.Integer('Bom个数', compute="_compute_count")
  19. bom_ids = fields.Many2many('wh.bom', string='Bom', compute="_compute_count")
  20. move_line_count = fields.Integer('调拨次数', compute="_compute_count")
  21. incoming_ids = fields.One2many(
  22. string='即将入库',
  23. comodel_name='wh.move.line',
  24. inverse_name='goods_id',
  25. domain=[('type','=','in'),('state','=','draft')],
  26. readonly=True,
  27. )
  28. outgoing_ids = fields.One2many(
  29. string='即将出库',
  30. comodel_name='wh.move.line',
  31. inverse_name='goods_id',
  32. domain=[('type','=','out'),('state','=','draft')],
  33. readonly=True,
  34. )
  35. available_qty = fields.Float('可用数量', compute='compute_stock_qty', digits='Quantity')
  36. # 使用SQL来取得指定商品情况下的库存数量
  37. def get_stock_qty(self):
  38. for Goods in self:
  39. self.env.cr.execute('''
  40. SELECT sum(line.qty_remaining) as qty,
  41. wh.name as warehouse
  42. FROM wh_move_line line
  43. LEFT JOIN warehouse wh ON line.warehouse_dest_id = wh.id
  44. WHERE line.qty_remaining != 0
  45. AND wh.type = 'stock'
  46. AND line.state = 'done'
  47. AND line.goods_id = %s
  48. GROUP BY wh.name
  49. ''' % (Goods.id,))
  50. return self.env.cr.dictfetchall()
  51. def compute_stock_qty(self):
  52. for g in self:
  53. g.current_qty = sum(line.get('qty') for line in g.get_stock_qty())
  54. g.available_qty = g.current_qty \
  55. + sum(l.goods_qty for l in g.incoming_ids) \
  56. - sum(l.goods_qty for l in g.outgoing_ids)
  57. def _get_cost(self, warehouse=None, ignore=None):
  58. # 如果没有历史的剩余数量,计算最后一条move的成本
  59. # 存在一种情况,计算一条line的成本的时候,先done掉该line,之后在通过该函数
  60. # 查询成本,此时百分百搜到当前的line,所以添加ignore参数来忽略掉指定的line
  61. self.ensure_one()
  62. if warehouse:
  63. domain = [
  64. ('state', '=', 'done'),
  65. ('goods_id', '=', self.id),
  66. ('warehouse_dest_id', '=', warehouse.id)
  67. ]
  68. if ignore:
  69. if isinstance(ignore, int):
  70. ignore = [ignore]
  71. domain.append(('id', 'not in', ignore))
  72. move = self.env['wh.move.line'].search(
  73. domain, limit=1, order='cost_time desc, id desc')
  74. if move:
  75. return move.cost_unit
  76. return self.cost
  77. def get_suggested_cost_by_warehouse(
  78. self, warehouse, qty, lot_id=None, attribute=None, ignore_move=None):
  79. # 存在一种情况,计算一条line的成本的时候,先done掉该line,之后在通过该函数
  80. # 查询成本,此时百分百搜到当前的line,所以添加ignore参数来忽略掉指定的line
  81. if lot_id:
  82. print(lot_id)
  83. records, cost = self.get_matching_records_by_lot( # 有批次:优先按批次去取成本;返回:匹配记录和成本
  84. lot_id, qty, suggested=True)
  85. else:
  86. records, cost = self.get_matching_records( # 没有批次:按 库位 就近、先到期先出、先进先出原则 ->查(wh.move.line)匹配记录去取成本
  87. warehouse, qty, attribute=attribute, ignore_stock=True, ignore=ignore_move)
  88. matching_qty = sum(record.get('qty') for record in records)
  89. if matching_qty:
  90. cost_unit = safe_division(cost, matching_qty)
  91. if matching_qty >= qty:
  92. return cost, cost_unit
  93. else:
  94. cost_unit = self._get_cost(warehouse, ignore=ignore_move)
  95. return cost_unit * qty, cost_unit
  96. def is_using_matching(self):
  97. """
  98. 是否需要获取匹配记录
  99. :return:
  100. """
  101. if self.no_stock:
  102. return False
  103. return True
  104. def is_using_batch(self):
  105. """
  106. 是否使用批号管理
  107. :return:
  108. """
  109. self.ensure_one()
  110. return self.using_batch
  111. def get_matching_records_by_lot(self, lot_id, qty, uos_qty=0, suggested=False):
  112. """
  113. 按批号来获取匹配记录
  114. :param lot_id: 明细中输入的批号
  115. :param qty: 明细中输入的数量
  116. :param uos_qty: 明细中输入的辅助数量
  117. :param suggested:
  118. :return: 匹配记录和成本
  119. """
  120. self.ensure_one()
  121. if not lot_id:
  122. raise UserError(_(u'批号没有被指定,无法获得成本'))
  123. if not suggested and lot_id.state != 'done':
  124. raise UserError(_(u'正在确认ID为{}的移库行。批号%s还没有实际入库,请先确认该入库')
  125. .format(lot_id.id, lot_id.move_id.name))
  126. decimal_quantity = self.env.ref('core.decimal_quantity')
  127. _logger.info('{} / {} / {} / {}'.format(lot_id.lot, qty, lot_id.qty_remaining, decimal_quantity.digits))
  128. if (float_compare(qty, lot_id.qty_remaining, decimal_quantity.digits) > 0
  129. and not self.env.context.get('wh_in_line_ids')):
  130. raise UserError(_(u'商品%s %s 批次 %s 的库存数量 %s 不够本次出库 %s') % (
  131. self.code and '[%s]' % self.code or '', self.name, lot_id.lot, lot_id.qty_remaining, qty))
  132. return [{'line_in_id': lot_id.id, 'qty': qty, 'uos_qty': uos_qty,
  133. 'expiration_date': lot_id.expiration_date}], \
  134. lot_id.get_real_cost_unit() * qty
  135. def get_matching_records(self, warehouse, qty, uos_qty=0, attribute=None,
  136. ignore_stock=False, ignore=None, move_line=False):
  137. """
  138. 获取匹配记录,不考虑批号
  139. :param ignore_stock: 当参数指定为True的时候,此时忽略库存警告
  140. :param ignore: 一个move_line列表,指定查询成本的时候跳过这些move
  141. :return: 匹配记录和成本
  142. """
  143. matching_records = []
  144. for Goods in self:
  145. domain = [
  146. ('qty_remaining', '>', 0),
  147. ('state', '=', 'done'),
  148. ('warehouse_dest_id', '=', warehouse.id),
  149. ('goods_id', '=', Goods.id)
  150. ]
  151. if ignore:
  152. if isinstance(ignore, int):
  153. domain.append(('id', 'not in', [ignore]))
  154. if attribute:
  155. domain.append(('attribute_id', '=', attribute.id))
  156. # 内部移库,从源库位移到目的库位,匹配时从源库位取值; location.py confirm_change 方法
  157. if self.env.context.get('location'):
  158. domain.append(
  159. ('location_id', '=', self.env.context.get('location')))
  160. # 出库单行 填写了库位
  161. if not self.env.context.get('location') and move_line and move_line.location_id:
  162. domain.append(('location_id', '=', move_line.location_id.id))
  163. """@zzx需要在大量数据的情况下评估一下速度"""
  164. # 出库顺序按 库位 就近、先到期先出、先进先出
  165. lines = self.env['wh.move.line'].search(
  166. domain, order='location_id, expiration_date, cost_time, id')
  167. qty_to_go, uos_qty_to_go, cost = qty, uos_qty, 0 # 分别为待出库商品的数量、辅助数量和成本
  168. for line in lines:
  169. if qty_to_go <= 0 and uos_qty_to_go <= 0:
  170. break
  171. matching_qty = min(line.qty_remaining, qty_to_go)
  172. matching_uos_qty = matching_qty / Goods.conversion
  173. matching_records.append({'line_in_id': line.id, 'expiration_date': line.expiration_date,
  174. 'qty': matching_qty, 'uos_qty': matching_uos_qty})
  175. cost += matching_qty * line.get_real_cost_unit()
  176. qty_to_go -= matching_qty
  177. uos_qty_to_go -= matching_uos_qty
  178. else:
  179. decimal_quantity = self.env.ref('core.decimal_quantity')
  180. if not ignore_stock and float_compare(qty_to_go, 0, decimal_quantity.digits) > 0 and not self.env.context.get('wh_in_line_ids'):
  181. raise UserError(_(u'商品%s %s的库存数量不够本次出库') % (Goods.code and '[%s]' % Goods.code or '', Goods.name,))
  182. if self.env.context.get('wh_in_line_ids'):
  183. domain = [('id', 'in', self.env.context.get('wh_in_line_ids')),
  184. ('state', '=', 'done'),
  185. ('warehouse_dest_id', '=', warehouse.id),
  186. ('goods_id', '=', Goods.id)]
  187. if attribute:
  188. domain.append(('attribute_id', '=', attribute.id))
  189. line_in_id = self.env['wh.move.line'].search(
  190. domain, order='expiration_date, cost_time, id')
  191. if line_in_id:
  192. matching_records.append({'line_in_id': line_in_id.id, 'expiration_date': line_in_id.expiration_date,
  193. 'qty': qty_to_go, 'uos_qty': uos_qty_to_go})
  194. return matching_records, cost
  195. _used_not_allowed_modification = ({'uom_id','uos_id','conversion'},
  196. '商品已被使用, 不允许修改单位或转化率')
  197. def write(self, vals):
  198. used_fields, used_msg = self._used_not_allowed_modification
  199. if len(used_fields)>0 and ( set(vals.keys()).intersection(used_fields) ):
  200. # 所有用到了商品的字段
  201. self.env.cr.execute("select imf.name, imf.model from goods_reference_black_list grbl "+
  202. "left join ir_model_fields imf on imf.model_id = grbl.ref_model_id" +
  203. " where imf.relation=%s " +
  204. "and imf.ttype in ('many2one') and imf.store=true;",
  205. ('goods', ))
  206. relation_fields = self.env.cr.fetchall()
  207. for goods in self:
  208. been_used = False
  209. # 所有用到了当前商品的记录
  210. for field, model in relation_fields:
  211. if not been_used:
  212. sql = "select id from " + model.replace('.', '_') + \
  213. " where " + field + "=" + str(goods.id) + ";"
  214. self.env.cr.execute(sql)
  215. if self.env.cr.fetchall():
  216. been_used = True
  217. if been_used:
  218. raise UserError(used_msg)
  219. return super().write(vals)
  220. def _compute_count(self):
  221. ''' 此商品作为组合件的BOM个数 '''
  222. for s in self:
  223. bom_lines = self.env['wh.bom.line'].search(
  224. [('goods_id', '=', s.id),
  225. ('type', '=', 'parent')])
  226. s.bom_ids = [(6, 0, list(set([l.bom_id.id for l in bom_lines])))]
  227. s.bom_count = len(s.bom_ids)
  228. move_lines = self.env['wh.move.line'].search(
  229. [('goods_id', '=', s.id),
  230. ('state', '=', 'done')])
  231. s.move_line_count = len(move_lines)
  232. def button_list_bom(self):
  233. return {
  234. 'name': _('%s 物料清单') % self.name,
  235. 'view_mode': 'list,form',
  236. 'res_model': 'wh.bom',
  237. 'type': 'ir.actions.act_window',
  238. 'domain': [('id', 'in', self.bom_ids.ids)],
  239. }
  240. def button_list_move(self):
  241. return {
  242. 'name': _('%s 库存调拨') % self.name,
  243. 'view_mode': 'list',
  244. 'res_model': 'wh.move.line',
  245. 'type': 'ir.actions.act_window',
  246. 'domain': [('goods_id', '=', self.id)],
  247. 'context': {'search_default_done':1}
  248. }
  249. class GoodsReferenceBlackList(models.Model):
  250. _name = 'goods.reference.black.list'
  251. _description = '商品引用检测黑名单'
  252. ref_model_id = fields.Many2one('ir.model', '模型', help='指定的模型若有引用商品的,该商品对应的单位或转化率无法进行修改')
上海开阖软件有限公司 沪ICP备12045867号-1