GoodERP
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

413 lines
17KB

  1. # Copyright 2016 上海开阖软件有限公司 (http://www.osbzr.com)
  2. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  3. from odoo import api, fields, models, _
  4. from odoo.exceptions import UserError, ValidationError
  5. # 字段只读状态
  6. READONLY_STATES = {
  7. 'done': [('readonly', True)],
  8. 'cancel': [('readonly', True)],
  9. }
  10. class Voucher(models.Model):
  11. '''新建凭证'''
  12. _name = 'voucher'
  13. _inherit = ['mail.thread']
  14. _order = 'period_id, name desc'
  15. _description = '会计凭证'
  16. @api.model
  17. def _default_voucher_date(self):
  18. return self._default_voucher_date_impl()
  19. @api.model
  20. def _default_voucher_date_impl(self):
  21. ''' 获得默认的凭证创建日期 '''
  22. voucher_setting = self.env['ir.default']._get(
  23. 'finance.config.settings', 'defaul_voucher_date')
  24. now_date = fields.Date.context_today(self)
  25. if voucher_setting == 'last' and self.search([], limit=1):
  26. create_date = self.search([], limit=1).date
  27. else:
  28. create_date = now_date
  29. return create_date
  30. @api.model
  31. def _select_objects(self):
  32. models = self.env['ir.model'].sudo().search([])
  33. return [(model.model, model.name) for model in models]
  34. @api.depends('date')
  35. def _compute_period_id(self):
  36. for v in self:
  37. v.period_id = self.env['finance.period'].get_period(v.date)
  38. document_word_id = fields.Many2one(
  39. 'document.word', '凭证字', ondelete='restrict', required=True,
  40. default=lambda self: self.env.ref('finance.document_word_1'))
  41. date = fields.Date('凭证日期', required=True, default=_default_voucher_date,
  42. tracking=True, help='本张凭证创建的时间', copy=False)
  43. name = fields.Char('凭证号', tracking=True, copy=False)
  44. att_count = fields.Integer(
  45. '附单据', default=1, help='原始凭证的张数')
  46. period_id = fields.Many2one(
  47. 'finance.period',
  48. '会计期间',
  49. compute='_compute_period_id',
  50. ondelete='restrict',
  51. store=True,
  52. help='本张凭证发生日期对应的,会计期间')
  53. line_ids = fields.One2many(
  54. 'voucher.line',
  55. 'voucher_id',
  56. '凭证明细',
  57. copy=True,)
  58. amount_text = fields.Float('总计', compute='_compute_amount', store=True,
  59. tracking=True, digits='Amount', help='凭证金额')
  60. state = fields.Selection([('draft', '草稿'),
  61. ('done', '已确认'),
  62. ('cancel', '已作废')], '状态', default='draft',
  63. index=True,
  64. tracking=True, help='凭证所属状态!')
  65. is_checkout = fields.Boolean('结账凭证', help='是否是结账凭证')
  66. is_init = fields.Boolean('是否初始化凭证', help='是否是初始化凭证')
  67. company_id = fields.Many2one(
  68. 'res.company',
  69. string='公司',
  70. change_default=True,
  71. default=lambda self: self.env.company)
  72. ref = fields.Reference(string='前置单据',
  73. selection='_select_objects')
  74. brief = fields.Char('摘要', related="line_ids.name", store=True)
  75. details = fields.Html('明细', compute='_compute_details')
  76. is_multi_currency = fields.Boolean('多币种', related='company_id.is_multi_currency')
  77. note = fields.Text('备注')
  78. @api.depends('line_ids')
  79. def _compute_details(self):
  80. for v in self:
  81. vl = {'col': ['往来单位', '科目', '借方', '贷方'], 'val': []}
  82. for line in v.line_ids:
  83. vl['val'].append([line.partner_id.name or '', line.account_id.name, line.debit, line.credit])
  84. v.details = v.company_id._get_html_table(vl)
  85. def voucher_done(self):
  86. """
  87. 确认 凭证按钮 所调用的方法
  88. :return: 主要是把 凭证的 state改变
  89. """
  90. for v in self:
  91. if v.state == 'done':
  92. raise UserError('凭证%s已经确认,请不要重复确认!' % v.name)
  93. if v.date < self.env.company.start_date:
  94. raise UserError('凭证日期不可早于启用日期')
  95. if v.period_id.is_closed:
  96. raise UserError('该会计期间已结账!不能确认')
  97. if not v.line_ids:
  98. raise ValidationError('请输入凭证行')
  99. for line in v.line_ids:
  100. if line.debit + line.credit == 0:
  101. raise ValidationError(
  102. '单行凭证行 %s 借和贷不能同时为0' % line.account_id.name)
  103. if line.debit * line.credit != 0:
  104. raise ValidationError(
  105. '单行凭证行不能同时输入借和贷\n 摘要为%s的凭证行 借方为:%s 贷方为:%s' %
  106. (line.name, line.debit, line.credit))
  107. debit_sum = sum([line.debit for line in v.line_ids])
  108. credit_sum = sum([line.credit for line in v.line_ids])
  109. precision = self.env['decimal.precision'].precision_get('Amount')
  110. debit_sum = round(debit_sum, precision)
  111. credit_sum = round(credit_sum, precision)
  112. if debit_sum != credit_sum:
  113. currency_debit_sum = sum([(line.currency_amount * line.rate_silent) for line in v.line_ids.filtered(
  114. lambda _l: _l and _l.currency_amount and _l.debit > _l.credit)])
  115. currency_debit_sum += sum([line.debit for line in v.line_ids.filtered(
  116. lambda _l: _l and not _l.currency_amount)])
  117. currency_credit_sum = sum([(line.currency_amount * line.rate_silent) for line in v.line_ids.filtered(
  118. lambda _l: _l and _l.currency_amount and _l.debit < _l.credit)])
  119. currency_credit_sum += sum([line.credit for line in v.line_ids.filtered(
  120. lambda _l: _l and not _l.currency_amount)])
  121. currency_debit_sum = round(currency_debit_sum, precision)
  122. currency_credit_sum = round(currency_credit_sum, precision)
  123. if currency_credit_sum != currency_debit_sum:
  124. raise ValidationError('借贷方不平,无法确认!\n 借方合计:%s 贷方合计:%s' % (debit_sum, credit_sum))
  125. diff = debit_sum - credit_sum
  126. if diff > 0:
  127. line_max = max(v.line_ids.filtered(lambda l: l.debit > 0), key=lambda l: l.debit)
  128. line_max.debit = line_max.debit - diff
  129. v.note = f'外币计算相同,疑似本位币计算进位出现问题,调整{line_max.name}的借方金额{diff}'
  130. else:
  131. line_max = max(v.line_ids.filtered(lambda l: l.credit > 0), key=lambda l: l.credit)
  132. line_max.credit = line_max.credit - diff
  133. v.note = f'外币计算相同,疑似本位币计算进位出现问题,调整{line_max.name}的贷方金额{diff}'
  134. v.state = 'done'
  135. if v.is_checkout: # 月结凭证不做反转
  136. return True
  137. for line in v.line_ids:
  138. if line.account_id.costs_types == 'out' and line.credit:
  139. # 费用类科目只能在借方记账,比如银行利息收入
  140. line.debit = -line.credit
  141. line.credit = 0
  142. if line.account_id.costs_types == 'in' and line.debit:
  143. # 收入类科目只能在贷方记账,比如退款给客户的情况
  144. line.credit = -line.debit
  145. line.debit = 0
  146. def voucher_can_be_draft(self):
  147. for v in self:
  148. if v.ref:
  149. raise UserError('不能撤销确认由其他单据生成的凭证!')
  150. self.voucher_draft()
  151. def voucher_draft(self):
  152. for v in self:
  153. if v.state == 'draft':
  154. raise UserError('凭证%s已经撤销确认,请不要重复撤销!' % v.name)
  155. if v.period_id.is_closed:
  156. raise UserError('%s期 会计期间已结账!不能撤销确认' % v.period_id.name)
  157. v.state = 'draft'
  158. @api.depends('line_ids')
  159. def _compute_amount(self):
  160. for v in self:
  161. v.amount_text = str(sum([line.debit for line in v.line_ids]))
  162. # 重载write 方法
  163. def write(self, vals):
  164. for order in self: # 还需要进一步优化
  165. if self.env.context.get('call_module', False) == "checkout_wizard":
  166. return super().write(vals)
  167. if order.period_id.is_closed is True:
  168. raise UserError('%s期 会计期间已结账,凭证不能再修改!' % order.period_id.name)
  169. return super().write(vals)
  170. class VoucherLine(models.Model):
  171. '''凭证明细'''
  172. _name = 'voucher.line'
  173. _description = '会计凭证明细'
  174. @api.model
  175. def _default_get(self, data):
  176. ''' 给明细行摘要、借方金额、贷方金额字段赋默认值 '''
  177. move_obj = self.env['voucher']
  178. total = 0.0
  179. context = self._context
  180. # odoo16没这个函数
  181. # if context.get('line_ids'):
  182. # for move_line_dict in move_obj.resolve_2many_commands(
  183. # 'line_ids', context.get('line_ids')):
  184. # data['name'] = data.get('name') or move_line_dict.get('name')
  185. # total += move_line_dict.get('debit', 0.0) - \
  186. # move_line_dict.get('credit', 0.0)
  187. # data['debit'] = total < 0 and -total or 0.0
  188. # data['credit'] = total > 0 and total or 0.0
  189. return data
  190. @api.model
  191. def default_get(self, fields):
  192. ''' 创建记录时,根据字段的 default 值和该方法给字段的赋值 来给 记录上的字段赋默认值 '''
  193. fields_data = super(VoucherLine, self).default_get(fields)
  194. data = self._default_get(fields_data)
  195. # 判断 data key是否在 fields 里,如果不在则删除该 key。程序员开发用
  196. for f in list(data.keys()):
  197. if f not in fields:
  198. del data[f]
  199. return data
  200. voucher_id = fields.Many2one('voucher', '对应凭证', ondelete='cascade')
  201. name = fields.Char('摘要', required=True, help='描述本条凭证行的缘由')
  202. account_id = fields.Many2one(
  203. 'finance.account', '会计科目',
  204. ondelete='restrict',
  205. required=True,
  206. domain="[('account_type','=','normal')]")
  207. debit = fields.Float('借方金额', digits='Amount',
  208. help='每条凭证行中只能记录借方金额或者贷方金额中的一个,\
  209. 一张凭证中所有的凭证行的借方余额,必须等于贷方余额。')
  210. credit = fields.Float('贷方金额', digits='Amount',
  211. help='每条凭证行中只能记录借方金额或者贷方金额中的一个,\
  212. 一张凭证中所有的凭证行的借方余额,必须等于贷方余额。')
  213. partner_id = fields.Many2one(
  214. 'partner', '往来单位', ondelete='restrict', help='凭证行的对应的往来单位')
  215. currency_amount = fields.Float('外币金额', digits='Amount')
  216. currency_id = fields.Many2one('res.currency', '外币币别', ondelete='restrict')
  217. is_multi_currency = fields.Boolean('多币种', related='company_id.is_multi_currency')
  218. rate_silent = fields.Float('汇率', digits=0)
  219. period_id = fields.Many2one(
  220. related='voucher_id.period_id', string='凭证期间', store=True)
  221. goods_qty = fields.Float('数量',
  222. digits='Quantity')
  223. goods_id = fields.Many2one('goods', '商品', ondelete='restrict')
  224. auxiliary_id = fields.Many2one(
  225. 'auxiliary.financing', '辅助核算', help='辅助核算是对账务处理的一种补充,即实现更广泛的账务处理,\
  226. 以适应企业管理和决策的需要.辅助核算一般通过核算项目来实现', ondelete='restrict')
  227. date = fields.Date(compute='_compute_voucher_date',
  228. store=True, string='凭证日期')
  229. state = fields.Selection(
  230. [('draft', '草稿'), ('done', '已确认'), ('cancel', '已作废')],
  231. compute='_compute_voucher_state',
  232. index=True,
  233. store=True, string='状态')
  234. init_obj = fields.Char('初始化对象', help='描述本条凭证行由哪个单证生成而来')
  235. company_id = fields.Many2one(
  236. 'res.company',
  237. string='公司',
  238. change_default=True,
  239. default=lambda self: self.env.company)
  240. @api.depends('voucher_id.date')
  241. def _compute_voucher_date(self):
  242. for vl in self:
  243. vl.date = vl.voucher_id.date
  244. @api.depends('voucher_id.state')
  245. def _compute_voucher_state(self):
  246. for vl in self:
  247. vl.state = vl.voucher_id.state
  248. @api.onchange('account_id')
  249. def onchange_account_id(self):
  250. self.currency_id = self.account_id.currency_id
  251. self.rate_silent = self.account_id.currency_id.rate
  252. res = {
  253. 'domain': {
  254. 'partner_id': [('name', '=', False)],
  255. 'goods_id': [('name', '=', False)],
  256. 'auxiliary_id': [('name', '=', False)]}}
  257. if not self.account_id or not self.account_id.auxiliary_financing:
  258. return res
  259. if self.account_id.auxiliary_financing == 'customer':
  260. res['domain']['partner_id'] = [('c_category_id', '!=', False)]
  261. elif self.account_id.auxiliary_financing == 'supplier':
  262. res['domain']['partner_id'] = [('s_category_id', '!=', False)]
  263. elif self.account_id.auxiliary_financing == 'goods':
  264. res['domain']['goods_id'] = []
  265. else:
  266. res['domain']['auxiliary_id'] = [
  267. ('type', '=', self.account_id.auxiliary_financing)]
  268. return res
  269. def view_document(self):
  270. self.ensure_one()
  271. return {
  272. 'name': '凭证',
  273. 'view_mode': 'form',
  274. 'res_model': 'voucher',
  275. 'res_id': self.voucher_id.id,
  276. 'type': 'ir.actions.act_window',
  277. }
  278. @api.constrains('account_id')
  279. def _check_account_id(self):
  280. for record in self:
  281. if record.account_id.account_type == 'view':
  282. raise UserError(_('不能往视图科目 %s-%s 记账,只能往下级科目记账!') % (self.account_id.code,self.account_id.name))
  283. def check_restricted_account(self):
  284. prohibit_account_debit_ids = self.env['finance.account'].search(
  285. [('restricted_debit', '=', True)])
  286. prohibit_account_credit_ids = self.env['finance.account'].search(
  287. [('restricted_credit', '=', True)])
  288. account_ids = []
  289. account = self.account_id
  290. account_ids.append(account)
  291. while account.parent_id:
  292. account_ids.append(account.parent_id)
  293. account = account.parent_id
  294. inner_account_debit = [
  295. acc for acc in account_ids if acc in prohibit_account_debit_ids]
  296. inner_account_credit = [
  297. acc for acc in account_ids if acc in prohibit_account_credit_ids]
  298. if self.debit and not self.credit and inner_account_debit:
  299. raise UserError(
  300. '借方禁止科目: %s-%s \n\n 提示:%s ' % (
  301. self.account_id.code,
  302. self.account_id.name,
  303. inner_account_debit[0].restricted_debit_msg))
  304. if not self.debit and self.credit and inner_account_credit:
  305. raise UserError(
  306. '贷方禁止科目: %s-%s \n\n 提示:%s ' % (
  307. self.account_id.code,
  308. self.account_id.name,
  309. inner_account_credit[0].restrict_credit_msg))
  310. @api.model_create_multi
  311. def create(self, values):
  312. """
  313. Create a new record for a model VoucherLine
  314. @param values: provides a data for new record
  315. @return: returns a id of new record
  316. """
  317. result = super(VoucherLine, self).create(values)
  318. if not self.env.context.get('entry_manual', False):
  319. return result
  320. for r in result:
  321. r.check_restricted_account()
  322. return result
  323. def write(self, values):
  324. """
  325. Update all record(s) in recordset, with new value comes as {values}
  326. return True on success, False otherwise
  327. @param values: dict of new values to be set
  328. @return: True on success, False otherwise
  329. """
  330. result = super(VoucherLine, self).write(values)
  331. if not self.env.context.get('entry_manual', False):
  332. return result
  333. for record in self:
  334. record.check_restricted_account()
  335. return result
  336. class DocumentWord(models.Model):
  337. '''凭证字'''
  338. _name = 'document.word'
  339. _description = '凭证字'
  340. name = fields.Char('凭证字')
  341. print_title = fields.Char('打印标题', help='凭证在打印时的显示的标题')
  342. company_id = fields.Many2one(
  343. 'res.company',
  344. string='公司',
  345. change_default=True,
  346. default=lambda self: self.env.company)
  347. class ChangeVoucherName(models.Model):
  348. ''' 修改凭证编号 '''
  349. _name = 'change.voucher.name'
  350. _description = '月末凭证变更记录'
  351. period_id = fields.Many2one('finance.period', '会计期间')
  352. before_voucher_name = fields.Char('以前凭证号')
  353. after_voucher_name = fields.Char('更新后凭证号')
  354. company_id = fields.Many2one(
  355. 'res.company',
  356. string='公司',
  357. change_default=True,
  358. default=lambda self: self.env.company)
上海开阖软件有限公司 沪ICP备12045867号-1