GoodERP
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

632 Zeilen
27KB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016 上海开阖软件有限公司 (http://www.osbzr.com)
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  4. from odoo import _
  5. from odoo.exceptions import UserError, ValidationError
  6. from odoo import fields, models, api
  7. from odoo.tools import float_compare, float_is_zero
  8. class MoneyOrder(models.Model):
  9. _name = 'money.order'
  10. _description = "收付款单"
  11. _inherit = ['mail.thread', 'mail.activity.mixin']
  12. _order = 'id desc'
  13. TYPE_SELECTION = [
  14. ('pay', '付款'),
  15. ('get', '收款'),
  16. ]
  17. @api.model_create_multi
  18. def create(self, vals_list):
  19. # 创建单据时,根据订单类型的不同,生成不同的单据编号
  20. for values in vals_list:
  21. if self.env.context.get('type') == 'pay':
  22. values.update(
  23. {'name': self.env['ir.sequence'].next_by_code('pay.order')})
  24. else:
  25. values.update(
  26. {'name': self.env['ir.sequence'].next_by_code('get.order')})
  27. # 创建时查找该业务伙伴是否存在 未审核 状态下的收付款单
  28. orders = self.env['money.order'].search([
  29. ('partner_id', '=', values.get('partner_id')),
  30. ('state', '=', 'draft'),
  31. ('source_ids', '!=', False),
  32. ('id', '!=', self.id)])
  33. if values.get('source_ids') and orders:
  34. raise UserError(_('该业务伙伴存在未确认的收/付款单,请先确认'))
  35. return super(MoneyOrder, self).create(vals_list)
  36. def write(self, values):
  37. # 修改时查找该业务伙伴是否存在 未审核 状态下的收付款单
  38. if values.get('partner_id'):
  39. orders = self.env['money.order'].search([
  40. ('partner_id', '=', values.get('partner_id')),
  41. ('state', '=', 'draft'),
  42. ('id', '!=', self.id)])
  43. if orders:
  44. raise UserError(_('业务伙伴(%s)存在未审核的收/付款单,请先审核') %
  45. orders.partner_id.name)
  46. return super(MoneyOrder, self).write(values)
  47. @api.depends('discount_amount',
  48. 'line_ids.amount',
  49. 'source_ids.this_reconcile')
  50. def _compute_advance_payment(self):
  51. """
  52. 计算字段advance_payment(本次预收)
  53. """
  54. for mo in self:
  55. amount, this_reconcile = 0.0, 0.0
  56. for line in mo.line_ids:
  57. amount += line.amount
  58. for line in mo.source_ids:
  59. this_reconcile += line.this_reconcile
  60. if mo.type == 'get':
  61. mo.advance_payment = \
  62. amount - this_reconcile + mo.discount_amount
  63. else:
  64. mo.advance_payment = \
  65. amount - this_reconcile - mo.discount_amount
  66. mo.amount = amount
  67. @api.depends('partner_id', 'type')
  68. def _compute_currency_id(self):
  69. """
  70. 取出币别
  71. :return:
  72. """
  73. for mo in self:
  74. partner_currency_id = (mo.type == 'get') \
  75. and mo.partner_id.c_category_id.account_id.currency_id.id \
  76. or mo.partner_id.s_category_id.account_id.currency_id.id
  77. mo.currency_id = \
  78. partner_currency_id or mo.env.user.company_id.currency_id.id
  79. state = fields.Selection([
  80. ('draft', '草稿'),
  81. ('done', '已完成'),
  82. ('cancel', '已作废'),
  83. ], string='状态', readonly=True, default='draft', copy=False, index=True,
  84. help='收/付款单状态标识,新建时状态为草稿;确认后状态为已完成')
  85. partner_id = fields.Many2one('partner', string='往来单位', required=True,
  86. ondelete='restrict',
  87. help='该单据对应的业务伙伴,单据确认时会影响他的应收应付余额')
  88. date = fields.Date(string='单据日期',
  89. default=lambda self: fields.Date.context_today(self),
  90. help='单据创建日期')
  91. name = fields.Char(string='单据编号', copy=False, readonly=True,
  92. help='单据编号,创建时会根据类型自动生成')
  93. note = fields.Text(string='备注', help='可以为该单据添加一些需要的标识信息')
  94. currency_id = fields.Many2one(
  95. 'res.currency',
  96. '币别',
  97. compute='_compute_currency_id',
  98. store=True,
  99. readonly=True,
  100. tracking=True,
  101. help='业务伙伴的类别科目上对应的外币币别')
  102. discount_amount = fields.Float(string='我方承担费用',
  103. readonly=False,
  104. digits='Amount',
  105. help='收/付款时发生的银行手续费或给业务伙伴的现金折扣。')
  106. discount_account_id = fields.Many2one(
  107. 'finance.account', '费用科目',
  108. domain="[('account_type','=','normal')]",
  109. readonly=False,
  110. help='收/付款单确认生成凭证时,手续费或折扣对应的科目')
  111. line_ids = fields.One2many('money.order.line', 'money_id',
  112. string='收/付款单行',
  113. help='收/付款单明细行')
  114. source_ids = fields.One2many('source.order.line', 'money_id',
  115. string='待核销行',
  116. help='收/付款单待核销行')
  117. type = fields.Selection(TYPE_SELECTION, string='类型',
  118. default=lambda self: self.env.context.get('type'),
  119. help='类型:收款单 或者 付款单')
  120. amount = fields.Float(string='总金额', compute='_compute_advance_payment',
  121. digits='Amount',
  122. store=True, readonly=True,
  123. help='收/付款单行金额总和')
  124. advance_payment = fields.Float(
  125. string='本次预付',
  126. compute='_compute_advance_payment',
  127. digits='Amount',
  128. store=True, readonly=True,
  129. help='根据收/付款单行金额总和,原始单据行金额总和及折扣额计算得来的预收/预付款,值>=0')
  130. to_reconcile = fields.Float(string='未核销金额',
  131. digits='Amount',
  132. help='未核销的预收/预付款金额')
  133. reconciled = fields.Float(string='已核销金额',
  134. digits='Amount',
  135. help='已核销的预收/预付款金额')
  136. origin_name = fields.Char('原始单据编号',
  137. help='原始单据编号')
  138. bank_name = fields.Char('开户行',
  139. help='开户行取自业务伙伴,可修改')
  140. bank_num = fields.Char('银行账号',
  141. help='银行账号取自业务伙伴,可修改')
  142. approve_uid = fields.Many2one('res.users', '确认人',
  143. copy=False, ondelete='restrict')
  144. approve_date = fields.Datetime('确认日期', copy=False)
  145. company_id = fields.Many2one(
  146. 'res.company',
  147. string='公司',
  148. change_default=True,
  149. default=lambda self: self.env.company)
  150. voucher_id = fields.Many2one('voucher',
  151. '对应凭证',
  152. readonly=True,
  153. ondelete='restrict',
  154. copy=False,
  155. help='收/付款单确认时生成的对应凭证')
  156. def create_reconcile(self):
  157. self.ensure_one()
  158. if self.env['money.invoice'].search([
  159. ('partner_id', '=', self.partner_id.id),
  160. ('state', '=', 'done'),
  161. ('to_reconcile', '!=', 0),
  162. ], limit=1):
  163. if self.type == 'get':
  164. business_type = 'adv_pay_to_get'
  165. else:
  166. business_type = 'adv_get_to_pay'
  167. recon = self.env['reconcile.order'].create({
  168. 'partner_id': self.partner_id.id,
  169. 'business_type': business_type})
  170. recon.onchange_partner_id()
  171. action = {
  172. 'name': _('核销单'),
  173. 'type': 'ir.actions.act_window',
  174. 'view_mode': 'form',
  175. 'res_model': 'reconcile.order',
  176. 'res_id': recon.id,
  177. }
  178. return action
  179. else:
  180. raise UserError(_('没有未核销结算单'))
  181. def write_off_reset(self):
  182. """
  183. 单据审核前重置计算单行上的本次核销金额
  184. :return:
  185. """
  186. self.ensure_one()
  187. if self.state != 'draft':
  188. raise ValueError(_('已确认的单据不能执行这个操作'))
  189. for source in self.source_ids:
  190. source.this_reconcile = 0
  191. return True
  192. @api.onchange('date')
  193. def onchange_date(self):
  194. """
  195. 当修改日期时,则根据context中的money的type对客户添加过滤,过滤出是供应商还是客户。
  196. (因为date有默认值所以这个过滤是默认触发的) 其实和date是否变化没有关系,页面加载就触发下面的逻辑
  197. :return:
  198. """
  199. if self.env.context.get('type') == 'get':
  200. return {'domain': {'partner_id': [('c_category_id', '!=', False)]}}
  201. else:
  202. return {'domain': {'partner_id': [('s_category_id', '!=', False)]}}
  203. def _get_source_line(self, invoice):
  204. """
  205. 根据传入的invoice的对象取出对应的值 构造出 source_line的一个dict 包含source line的主要参数
  206. :param invoice: money_invoice对象
  207. :return: dict
  208. """
  209. return {
  210. 'name': invoice.id,
  211. 'category_id': invoice.category_id.id,
  212. 'amount': invoice.amount,
  213. 'date': invoice.date,
  214. 'reconciled': invoice.reconciled,
  215. 'to_reconcile': invoice.to_reconcile,
  216. 'this_reconcile': invoice.to_reconcile,
  217. 'date_due': invoice.date_due,
  218. }
  219. def _get_invoice_search_list(self):
  220. """
  221. 构造出 invoice 搜索的domain
  222. :return:
  223. """
  224. invoice_search_list = [('partner_id', '=', self.partner_id.id),
  225. ('to_reconcile', '!=', 0),
  226. ('state', '=', 'done')]
  227. if self.env.context.get('type') == 'get':
  228. invoice_search_list.append(('category_id.type', '=', 'income'))
  229. else: # type = 'pay':
  230. invoice_search_list.append(('category_id.type', '=', 'expense'))
  231. return invoice_search_list
  232. @api.onchange('partner_id')
  233. def onchange_partner_id(self):
  234. """
  235. 对partner修改的监控当 partner 修改时,
  236. 就对 页面相对应的字段进行修改(bank_name,bank_num,source_ids)
  237. """
  238. if not self.partner_id:
  239. return {}
  240. self.source_ids = False
  241. source_lines = []
  242. self.bank_name = self.partner_id.bank_name
  243. self.bank_num = self.partner_id.bank_num
  244. for invoice in self.env['money.invoice'].search(
  245. self._get_invoice_search_list()):
  246. source_lines.append((0, 0, self._get_source_line(invoice)))
  247. self.source_ids = source_lines
  248. def money_order_done(self):
  249. '''对收付款单的审核按钮'''
  250. for order in self:
  251. if order.state == 'done':
  252. raise UserError(_('请不要重复确认'))
  253. if order.type == 'pay' and \
  254. not order.partner_id.s_category_id.account_id:
  255. raise UserError(_('请输入供应商类别(%s)上的科目') %
  256. order.partner_id.s_category_id.name)
  257. if order.type == 'get' and \
  258. not order.partner_id.c_category_id.account_id:
  259. raise UserError(_('请输入客户类别(%s)上的科目') %
  260. order.partner_id.c_category_id.name)
  261. if order.advance_payment < 0 and order.source_ids:
  262. raise UserError(_('本次核销金额不能大于付款金额。\n差额: %s') %
  263. (order.advance_payment))
  264. total = 0
  265. for line in order.line_ids:
  266. rate_silent = self.env['res.currency'].get_rate_silent(
  267. order.date, line.currency_id.id)
  268. if order.type == 'pay': # 付款账号余额减少, 退款账号余额增加
  269. decimal_amount = self.env.ref('core.decimal_amount')
  270. balance = (
  271. line.currency_id
  272. != self.env.user.company_id.currency_id
  273. and line.bank_id.currency_amount
  274. or line.bank_id.balance)
  275. if float_compare(
  276. balance, line.amount,
  277. precision_digits=decimal_amount.digits) == -1:
  278. raise UserError(_('账户余额不足。\n账户余额:%s 付款行金额:%s') %
  279. (balance, line.amount))
  280. if (line.currency_id
  281. != self.env.user.company_id.currency_id): # 外币
  282. line.bank_id.currency_amount -= line.amount
  283. line.bank_id.balance -= line.amount * rate_silent
  284. else:
  285. line.bank_id.balance -= line.amount
  286. else: # 收款账号余额增加, 退款账号余额减少
  287. if (line.currency_id
  288. != self.env.user.company_id.currency_id): # 外币
  289. line.bank_id.currency_amount += line.amount
  290. line.bank_id.balance += line.amount * rate_silent
  291. else:
  292. line.bank_id.balance += line.amount
  293. total += line.amount
  294. if order.type == 'pay':
  295. order.partner_id.payable -= total - order.discount_amount
  296. else:
  297. order.partner_id.receivable -= total + order.discount_amount
  298. # 更新结算单的未核销金额、已核销金额
  299. for source in order.source_ids:
  300. decimal_amount = self.env.ref('core.decimal_amount')
  301. if float_compare(
  302. source.this_reconcile,
  303. abs(source.to_reconcile),
  304. precision_digits=decimal_amount.digits) == 1:
  305. raise UserError(
  306. _('本次核销金额不能大于未核销金额。\n 核销金额:%s 未核销金额:%s')
  307. % (abs(source.to_reconcile), source.this_reconcile))
  308. source.name.to_reconcile -= source.this_reconcile
  309. source.name.reconciled += source.this_reconcile
  310. if source.this_reconcile == 0: # 如果核销行的本次付款金额为0,删除
  311. source.unlink()
  312. # 生成凭证并审核
  313. if order.type == 'get':
  314. voucher = order.create_money_order_get_voucher(
  315. order.line_ids, order.source_ids, order.partner_id,
  316. order.name, order.note or '')
  317. else:
  318. voucher = order.create_money_order_pay_voucher(
  319. order.line_ids, order.source_ids, order.partner_id,
  320. order.name, order.note or '')
  321. voucher.voucher_done()
  322. return order.write({
  323. 'to_reconcile': order.advance_payment,
  324. 'reconciled': order.amount - order.advance_payment,
  325. 'voucher_id': voucher.id,
  326. 'approve_uid': self.env.uid,
  327. 'approve_date': fields.Datetime.now(self),
  328. 'state': 'done',
  329. })
  330. def money_order_draft(self):
  331. """
  332. 收付款单反审核方法
  333. """
  334. for order in self:
  335. if order.state == 'draft':
  336. raise UserError(_('请不要重复撤销 %s') % self._description)
  337. # 收/付款单 存在已审核金额不为0的核销单
  338. total_current_reconciled = order.amount - order.advance_payment
  339. decimal_amount = self.env.ref('core.decimal_amount')
  340. if float_compare(order.reconciled,
  341. total_current_reconciled,
  342. precision_digits=decimal_amount.digits) != 0:
  343. raise UserError(_('单据已核销金额不为0,不能反审核!请检查核销单!'))
  344. total = 0
  345. for line in order.line_ids:
  346. rate_silent = self.env['res.currency'].get_rate_silent(
  347. order.date, line.currency_id.id)
  348. if order.type == 'pay': # 反审核:付款账号余额增加
  349. if (line.currency_id
  350. != self.env.user.company_id.currency_id): # 外币
  351. line.bank_id.currency_amount += line.amount
  352. line.bank_id.balance += line.amount * rate_silent
  353. else:
  354. line.bank_id.balance += line.amount
  355. else: # 反审核:收款账号余额减少
  356. balance = (
  357. line.currency_id
  358. != self.env.user.company_id.currency_id
  359. and line.bank_id.currency_amount
  360. or line.bank_id.balance)
  361. decimal_amount = self.env.ref('core.decimal_amount')
  362. if float_compare(
  363. balance,
  364. line.amount,
  365. precision_digits=decimal_amount.digits) == -1:
  366. raise UserError(_('账户余额不足。\n 账户余额:%s 收款行金额:%s') %
  367. (balance, line.amount))
  368. if (line.currency_id
  369. != self.env.user.company_id.currency_id): # 外币
  370. line.bank_id.currency_amount -= line.amount
  371. line.bank_id.balance -= line.amount * rate_silent
  372. else:
  373. line.bank_id.balance -= line.amount
  374. total += line.amount
  375. if order.type == 'pay':
  376. order.partner_id.payable += total - order.discount_amount
  377. else:
  378. order.partner_id.receivable += total + order.discount_amount
  379. for source in order.source_ids:
  380. source.name.to_reconcile += source.this_reconcile
  381. source.name.reconciled -= source.this_reconcile
  382. voucher = order.voucher_id
  383. order.write({
  384. 'to_reconcile': 0,
  385. 'reconciled': 0,
  386. 'voucher_id': False,
  387. 'approve_uid': False,
  388. 'approve_date': False,
  389. 'state': 'draft',
  390. })
  391. # 反审核凭证并删除
  392. if voucher.state == 'done':
  393. voucher.voucher_draft()
  394. voucher.unlink()
  395. return True
  396. def _prepare_vouch_line_data(self, line, name, account_id, debit, credit,
  397. voucher_id, partner_id, currency_id):
  398. rate_silent = currency_amount = 0
  399. if currency_id:
  400. rate_silent = self.env['res.currency'].get_rate_silent(
  401. self.date, currency_id)
  402. currency_amount = debit or credit
  403. debit = debit * (rate_silent or 1)
  404. credit = credit * (rate_silent or 1)
  405. return {
  406. 'name': name,
  407. 'account_id': account_id,
  408. 'debit': debit,
  409. 'credit': credit,
  410. 'voucher_id': voucher_id,
  411. 'partner_id': partner_id,
  412. 'currency_id': currency_id,
  413. 'currency_amount': currency_amount,
  414. 'rate_silent': rate_silent or ''
  415. }
  416. def _create_voucher_line(self, line, name, account_id, debit, credit,
  417. voucher_id, partner_id, currency_id):
  418. line_data = self._prepare_vouch_line_data(
  419. line, name, account_id, debit, credit,
  420. voucher_id, partner_id, currency_id)
  421. voucher_line = self.env['voucher.line'].create(line_data)
  422. return voucher_line
  423. def create_money_order_get_voucher(self, line_ids, source_ids,
  424. partner, name, note):
  425. """
  426. 为收款单创建凭证
  427. :param line_ids: 收款单明细
  428. :param source_ids: 没用到
  429. :param partner: 客户
  430. :param name: 收款单名称
  431. :return: 创建的凭证
  432. """
  433. vouch_obj = self.env['voucher'].sudo().create(
  434. {'date': self.date, 'ref': '%s,%s' % (self._name, self.id)})
  435. # self.write({'voucher_id': vouch_obj.id})
  436. amount_all = 0.0
  437. line_data = False
  438. for line in line_ids:
  439. line_data = line
  440. if not line.bank_id.account_id:
  441. raise UserError(f'请配置结算账户[{line.bank_id.name}]]的会计科目')
  442. # 生成借方明细行
  443. if line.amount: # 可能输入金额为0的收款单用于核销尾差
  444. self._create_voucher_line(line,
  445. "%s %s" % (name, note),
  446. line.bank_id.account_id.id,
  447. line.amount,
  448. 0,
  449. vouch_obj.id,
  450. '',
  451. line.currency_id.id
  452. )
  453. amount_all += line.amount
  454. if self.discount_amount != 0:
  455. # 生成借方明细行
  456. self._create_voucher_line(
  457. False,
  458. "%s 现金折扣 %s" % (name, note),
  459. self.discount_account_id.id,
  460. self.discount_amount,
  461. 0,
  462. vouch_obj.id,
  463. self.partner_id.id,
  464. line_data and line_data.currency_id.id or self.currency_id.id
  465. )
  466. if partner.c_category_id:
  467. partner_account_id = partner.c_category_id.account_id.id
  468. # 生成贷方明细行
  469. if amount_all + self.discount_amount:
  470. self._create_voucher_line(
  471. '',
  472. "%s %s" % (name, note),
  473. partner_account_id,
  474. 0,
  475. amount_all + self.discount_amount,
  476. vouch_obj.id,
  477. self.partner_id.id,
  478. line_data and line.currency_id.id or self.currency_id.id
  479. )
  480. return vouch_obj
  481. def create_money_order_pay_voucher(self, line_ids, source_ids,
  482. partner, name, note):
  483. """
  484. 为付款单创建凭证
  485. :param line_ids: 付款单明细
  486. :param source_ids: 没用到
  487. :param partner: 供应商
  488. :param name: 付款单名称
  489. :return: 创建的凭证
  490. """
  491. vouch_obj = self.env['voucher'].sudo().create(
  492. {'date': self.date, 'ref': '%s,%s' % (self._name, self.id)})
  493. # self.write({'voucher_id': vouch_obj.id})
  494. amount_all = 0.0
  495. line_data = False
  496. for line in line_ids:
  497. line_data = line
  498. if not line.bank_id.account_id:
  499. raise UserError(_('请配置结算账户[{line.bank_id.name}]]的会计科目'))
  500. # 生成贷方明细行 credit
  501. if line.amount: # 支持金额为0的付款用于核销尾差
  502. self._create_voucher_line(line,
  503. "%s %s" % (name, note),
  504. line.bank_id.account_id.id,
  505. 0,
  506. line.amount,
  507. vouch_obj.id,
  508. '',
  509. line.currency_id.id
  510. )
  511. amount_all += line.amount
  512. partner_account_id = partner.s_category_id.account_id.id
  513. # 生成借方明细行 debit
  514. if amount_all - self.discount_amount:
  515. self._create_voucher_line(
  516. '',
  517. "%s %s" % (name, note),
  518. partner_account_id,
  519. amount_all - self.discount_amount,
  520. 0,
  521. vouch_obj.id,
  522. self.partner_id.id,
  523. line_data and line.currency_id.id or self.currency_id.id
  524. )
  525. if self.discount_amount != 0:
  526. # 生成借方明细行 debit
  527. self._create_voucher_line(
  528. line_data and line_data or False,
  529. "%s 手续费 %s" % (name, note),
  530. self.discount_account_id.id,
  531. self.discount_amount,
  532. 0,
  533. vouch_obj.id,
  534. self.partner_id.id,
  535. line_data and line.currency_id.id or self.currency_id.id
  536. )
  537. return vouch_obj
  538. class MoneyOrderLine(models.Model):
  539. _name = 'money.order.line'
  540. _description = '收付款单明细'
  541. @api.depends('bank_id')
  542. def _compute_currency_id(self):
  543. """
  544. 获取币别
  545. """
  546. for mol in self:
  547. mol.currency_id = mol.bank_id.account_id.currency_id.id \
  548. or mol.env.user.company_id.currency_id.id
  549. if mol.bank_id and mol.currency_id != mol.money_id.currency_id:
  550. raise ValidationError(
  551. _('结算帐户与业务伙伴币别不一致。\n 结算账户币别:%s 业务伙伴币别:%s')
  552. % (mol.currency_id.name, mol.money_id.currency_id.name))
  553. money_id = fields.Many2one('money.order', string='收付款单',
  554. ondelete='cascade',
  555. help='订单行对应的收付款单')
  556. bank_id = fields.Many2one('bank.account', string='结算账户',
  557. required=True, ondelete='restrict',
  558. help='本次收款/付款所用的计算账户,确认收付款单会修改对应账户的金额')
  559. amount = fields.Float(string='金额',
  560. digits='Amount',
  561. help='本次结算金额')
  562. mode_id = fields.Many2one('settle.mode', string='结算方式',
  563. ondelete='restrict',
  564. help='结算方式:支票、信用卡等')
  565. currency_id = fields.Many2one(
  566. 'res.currency', '币别',
  567. compute='_compute_currency_id',
  568. store=True, readonly=True,
  569. help='结算账户对应的外币币别')
  570. number = fields.Char(string='结算号',
  571. help='本次结算号')
  572. note = fields.Char(string='备注',
  573. help='可以为本次结算添加一些需要的标识信息')
  574. company_id = fields.Many2one(
  575. 'res.company',
  576. string='公司',
  577. change_default=True,
  578. default=lambda self: self.env.company)
上海开阖软件有限公司 沪ICP备12045867号-1