sales.py
24.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
import re
from collections import defaultdict
from sqlite3 import DatabaseError
from odoo import api, fields, models, _
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import format_date
from psycopg2 import sql, DatabaseError
import logging
_logger = logging.getLogger(__name__)
class BphtbSales(models.Model):
_name = 'bphtb.sales'
_description = 'Transaksi BPHTB'
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin', 'sequence.mixin']
name = fields.Char(string='Number', copy=False, compute='_compute_name', readonly=False,
store=True, index=True, tracking=True)
ref = fields.Char(string='Reference', copy=False, tracking=True)
#
# rate = fields.Float(required=True)
# disc = fields.Float(required=True)
# min_omzet = fields.Integer(required=True)
code = fields.Char()
date = fields.Date(readonly=True,
states={'draft': [('readonly', False)]}, )
ppat_id = fields.Many2one('res.partner',
readonly=True,
states={'draft': [('readonly', False)]},
default=lambda self: self.env.user.partner_id.id
if not self.ppat_id else False)
wp_id = fields.Many2one('res.partner', required=True, readonly=True,
states={'draft': [('readonly', False)]}, )
wp_name = fields.Char(related='wp_id.name', string="Wajib Pajak", store=True)
wp_identity_number = fields.Char(related='wp_id.identity_number',
string="No Identitas", store=True)
wp_street = fields.Char(compute='_compute_wp', store=False)
wp_street2 = fields.Char(compute='_compute_wp', store=False)
wp_village_id = fields.Many2one('res.district.village', compute='_compute_wp', store=False,
string="WP Desa/Kelurahan")
wp_sub_district_id = fields.Many2one('res.district.sub', compute='_compute_wp',
store=False, string="WP Kecamatan")
wp_district_id = fields.Many2one('res.district', compute='_compute_wp',
store=False, string="WP Kota/Kab")
wp_state_id = fields.Many2one('res.country.state', compute='_compute_wp',
store=False, string="WP Provinsi")
wp_zip = fields.Char(compute='_compute_wp', string="WP ZIP", store=False)
wp_phone = fields.Char(compute='_compute_wp', string="WP Phone", store=False)
wp_email = fields.Char(compute='_compute_wp', string="WP Email", store=False)
wp_website = fields.Char(compute='_compute_wp', string="WP Web Site", store=False)
seller_id = fields.Many2one('res.partner', readonly=True,
states={'draft': [('readonly', False)]}, required=True)
seller_name = fields.Char(related='wp_id.name', string="Wajib Pajak", store=True)
seller_identity_number = fields.Char(related='wp_id.identity_number',
string="No Identitas", store=True)
seller_street = fields.Char(compute='_compute_seller', store=False)
seller_street2 = fields.Char(compute='_compute_seller', store=False)
seller_village_id = fields.Many2one('res.district.village', compute='_compute_seller', store=False,
string="WP Desa/Kelurahan")
seller_sub_district_id = fields.Many2one('res.district.sub', compute='_compute_seller',
store=False, string="Penjual Kecamatan")
seller_district_id = fields.Many2one('res.district', compute='_compute_seller',
store=False, string="Penjual Kota/Kab")
seller_state_id = fields.Many2one('res.country.state', compute='_compute_seller',
store=False, string="Penjual Provinsi")
seller_zip = fields.Char(compute='_compute_seller', string="Penjual ZIP", store=False)
seller_phone = fields.Char(compute='_compute_seller', string="Penjual Phone", store=False)
seller_email = fields.Char(compute='_compute_seller', string="Penjual Email", store=False)
seller_website = fields.Char(compute='_compute_seller', string="Penjual Web Site", store=False)
state = fields.Selection([('draft', 'Draft'),
('confirmed', 'Confirmed'),
('canceled', 'Canceled'),
], default='draft')
nop = fields.Char(size=18, readonly=True,
states={'draft': [('readonly', False)]}, )
tax_year = fields.Integer(readonly=True,
states={'draft': [('readonly', False)]}, )
# kd_propinsi = Column(String(2), nullable=False)
# kd_dati2 = Column(String(2), nullable=False)
# kd_kecamatan = Column(String(3), nullable=False)
# kd_kelurahan = Column(String(3), nullable=False)
# kd_blok = Column(String(3), nullable=False)
# no_urut = Column(String(4), nullable=False)
# kd_jns_op = Column(String(1), nullable=False)
# op_alamat = Column(String(128), nullable=False)
# op_blok_kav = Column(String(128), nullable=False)
# op_rt = Column(String(3), nullable=False)
# op_rw = Column(String(3), nullable=False)
luas_bumi = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
luas_bng = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
njop_bumi = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
njop_bng = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
luas_bumi_bersama = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
luas_bng_bersama = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
njop_bumi_bersama = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
njop_bng_bersama = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
certicate_no = fields.Char(readonly=True,
states={'draft': [('readonly', False)]}, )
njop = fields.Float(compute="_compute_njop", store=True)
jenis_id = fields.Many2one('bphtb.jenis', readonly=True,
states={'draft': [('readonly', False)]}, )
rate = fields.Float(related='jenis_id.rate', store=True, readonly=True)
min_omzet = fields.Float(related='jenis_id.min_omzet', store=True, readonly=True)
disc_sk = fields.Char(required=False, readonly=True,
states={'draft': [('readonly', False)]}, )
disc = fields.Float(related='jenis_id.disc', store=True, readonly=True)
npop = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
basic_calc = fields.Float(compute="_compute_basic_calc")
npopkp = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
basic = fields.Float()
fine = fields.Float()
amount = fields.Float()
disc_amount = fields.Float()
payment = fields.Float(readonly=True,
states={'draft': [('readonly', False)]}, )
owed = fields.Float(compute="_compute_owed", store=True)
typ = fields.Selection([
('sspd', 'SSPD'),
('kb', 'SKPD KB'),
('kbt', 'SKPD KBT'),
('lb', 'SKPD LB'),
('nihil', 'SKPD Nihil'),
])
# file01 = Column(String(128))
# file02 = Column(String(128))
# file03 = Column(String(128))
# file04 = Column(String(128))
# file05 = Column(String(128))
# file06 = Column(String(128))
# file07 = Column(String(128))
# file08 = Column(String(128))
# file09 = Column(String(128))
# file10 = Column(String(128))
verification_date = fields.Date()
verification_no = fields.Char()
verification_uid = fields.Integer()
printed = fields.Boolean(default=False)
company_id = fields.Many2one('res.company',
default=lambda self: self.env.company.id
if not self.company_id else False)
district_id = fields.Many2one('res.district', related='company_id.district_id', store=True)
wp_ids = fields.Many2many('res.partner', readonly=True,
states={'draft': [('readonly', False)]}, )
def _my_check_method(self, cr, uid, ids, context=None):
# Your code goes here
return True or False
# _sql_constraints = [
# ('company_code_uniq', 'unique (company_id, code)', 'Kode harus unik')
# ]
# Prosesing Wajib Pajak
def _get_wp_field_names(self):
return [
("wp_identity_number", "identity_number"),
("wp_street", "street"),
("wp_street2", "street2"),
("wp_village_id", "village_id"),
("wp_sub_district_id", "sub_district_id"),
("wp_district_id", "district_id"),
("wp_state_id", "state_id"),
("wp_phone", "phone"),
("wp_email", "email"),
("wp_website", "website"),
("wp_zip", "zip"),
]
def _get_wp_update(self, partner):
if partner:
field = dict((key, partner[value])
for key, value in self._get_wp_field_names())
else:
field = dict((key, False)
for key, value in self._get_wp_field_names())
return field
def _compute_wp(self):
if self.wp_id:
for bphtb_sales in self.filtered(lambda bphtb_sales: bphtb_sales.wp_id):
address_data = bphtb_sales.wp_id.sudo().address_get(adr_pref=['wp'])
if address_data['wp']:
partner = bphtb_sales.wp_id.browse(address_data['wp']).sudo()
bphtb_sales.update(bphtb_sales._get_wp_update(partner))
else:
bphtb_sales.update(bphtb_sales._get_wp_update(False))
else:
self.update(self._get_wp_update(False))
# Seller
def _get_seller_field_names(self):
return [
("seller_identity_number", "identity_number"),
("seller_street", "street"),
("seller_street2", "street2"),
("seller_village_id", "village_id"),
("seller_sub_district_id", "sub_district_id"),
("seller_district_id", "district_id"),
("seller_state_id", "state_id"),
("seller_phone", "phone"),
("seller_email", "email"),
("seller_website", "website"),
("seller_zip", "zip"),
]
def _get_seller_update(self, partner):
if partner:
field = dict((key, partner[value])
for key, value in self._get_seller_field_names())
else:
field = dict((key, False)
for key, value in self._get_seller_field_names())
return field
def _compute_seller(self):
for rec in self:
if rec.seller_id:
for bphtb_sales in rec.filtered(lambda bphtb_sales: bphtb_sales.seller_id):
address_data = bphtb_sales.seller_id.sudo().address_get(adr_pref=['wp'])
if address_data['wp']:
partner = bphtb_sales.seller_id.browse(address_data['wp']).sudo()
bphtb_sales.update(bphtb_sales._get_seller_update(partner))
else:
bphtb_sales.update(bphtb_sales._get_seller_update(False))
else:
rec.update(rec._get_seller_update(False))
@api.depends('njop', 'npop', 'min_omzet')
def _compute_basic_calc(self):
for rec in self:
if rec.npop < rec.njop and rec.jenis_id.under_value:
rec.basic_calc = rec.npop
else:
rec.basic_calc = rec.npop if rec.npop > rec.njop else rec.njop
@api.depends('disc', 'payment', 'basic_calc')
def _compute_owed(self):
for rec in self:
rec.npopkp = rec.basic_calc - rec.min_omzet
rec.npopkp = 0 if rec.npopkp < 0 else rec.npopkp
rec.basic = rec.npopkp * rec.rate / 100
rec.amount = rec.basic + rec.fine
rec.disc_amount = rec.disc / 100 * rec.amount
rec.owed = rec.amount - rec.disc_amount - rec.payment
@api.depends('njop_bumi_bersama', 'njop_bng_bersama', 'njop_bumi', 'njop_bng')
def _compute_njop(self):
for rec in self:
rec.njop = rec.njop_bng + rec.njop_bumi + rec.njop_bumi_bersama + \
rec.njop_bng_bersama
@api.depends('name', 'state')
def name_get(self):
_logger.info("Name Get")
result = []
for move in self:
if self._context.get('name_groupby'):
name = '**%s**, %s' % (format_date(self.env, move.date), move._get_sales_display_name())
if move.ref:
name += ' (%s)' % move.ref
if move.ppat_id.name:
name += ' - %s' % move.ppat_id.name
else:
name = move._get_sales_display_name(show_ref=True)
result.append((move.id, name))
return result
@api.depends('state', 'date')
def _compute_name(self):
_logger.info('Compute Name')
def journal_key(move):
return (move.id, move.typ)
def date_key(move):
return (move.date.year, move.date.month)
grouped = defaultdict( # key: journal_id, move_type
lambda: defaultdict( # key: first adjacent (date.year, date.month)
lambda: {
'records': self.env['bphtb.sales'],
'format': False,
'format_values': False,
'reset': False
}
)
)
self = self.sorted(lambda m: (m.typ, m.company_id, m.date, m.ref or '', m.id,))
highest_name = self[0]._get_last_sequence() if self else False
# Group the moves by journal and month
for move in self:
if not highest_name and move == self[0] and move.date: # not move.posted_before and
# In the form view, we need to compute a default sequence so that the user can edit
# it. We only check the first move as an approximation (enough for new in form view)
pass
elif (move.name and move.name != '/') or move.state != 'confirmed':
try:
# if not move.posted_before:
move._constrains_date_sequence()
# Has already a name or is not posted, we don't add to a batch
continue
except ValidationError:
# Has never been posted and the name doesn't match the date: recompute it
pass
group = grouped[journal_key(move)][date_key(move)] #
_logger.info(group)
_logger.info(group['records'])
if not group['records']:
# Compute all the values needed to sequence this whole group
move._set_next_sequence()
group['format'], group['format_values'] = move._get_sequence_format_param(move.name)
group['reset'] = move._deduce_sequence_number_reset(move.name)
group['records'] += move
_logger.info(group)
# Fusion the groups depending on the sequence reset and the format used because `seq` is
# the same counter for multiple groups that might be spread in multiple months.
final_batches = []
for journal_group in grouped.values():
journal_group_changed = True
_logger.info(f"Jurnal Group {journal_group}")
for date_group in journal_group.values():
_logger.info(f"Date Group {date_group}")
if (journal_group_changed or final_batches[-1]['format'] != date_group['format']
or dict(final_batches[-1]['format_values'], seq=0) != dict(date_group['format_values'], seq=0)
):
final_batches += [date_group]
journal_group_changed = False
elif date_group['reset'] == 'never':
final_batches[-1]['records'] += date_group['records']
elif (
date_group['reset'] == 'year'
and final_batches[-1]['records'][0].date.year == date_group['records'][0].date.year
):
final_batches[-1]['records'] += date_group['records']
else:
final_batches += [date_group]
# Give the name based on previously computed values
for batch in final_batches:
for move in batch['records']:
move.name = batch['format'].format(**batch['format_values'])
batch['format_values']['seq'] += 1
batch['records']._compute_split_sequence()
_logger.info(batch['records'])
self.filtered(lambda m: not m.name).name = '/'
# @api.depends('njop')
# def njop_change(self):
def action_confirm(self):
_logger.info("Confirm")
to_post = self
if not self.env.su and not self.env.user.has_group('idg_bphtb.group_bphtb_ppat'):
raise AccessError(_("You don't have the access rights to confirm an sspd."))
for sales in to_post:
if sales.state == 'confirmed':
raise UserError(_('The entry %s (id %s) is already confirmed.') % (sales.name, sales.id))
if not sales.wp_id or not sales.seller_id:
raise UserError(
_("The field 'Wajib Pajak' and 'Penjual' is required."))
if not sales.date or sales.date > fields.Date.context_today(self):
raise UserError(_("The request date is required to validate this document."))
if not sales.date or sales.date < fields.Date.context_today(self):
raise UserError(_("Check request date."))
# to_post.mapped('line_ids').create_analytic_lines()
to_post.write({
'state': 'confirmed',
# 'posted_before': True,
})
for sales in to_post:
sales.message_subscribe([p.id for p in [sales.ppat_id] if p not in sales.sudo().message_partner_ids])
customer_count, supplier_count = defaultdict(int), defaultdict(int)
for sales in to_post:
customer_count[sales.ppat_id] += 1
for partner, count in customer_count.items():
(partner | partner.commercial_partner_id)._increase_rank('customer_rank', count)
# Trigger action for paid invoices in amount is zero
# to_post.filtered(
# lambda m: m.is_invoice(include_receipts=True) and m.currency_id.is_zero(m.amount_total)
# ).action_invoice_paid()
# Force balance check since nothing prevents another module to create an incorrect entry.
# This is performed at the very end to avoid flushing fields before the whole processing.
return to_post
def action_cancel(self):
self.state = 'canceled'
def action_draft(self):
self.state = 'draft'
self.name = '/'
def action_print(self):
if not self.printed:
self.printed = True
data = {
# 'from_date': self.wp_name,
# 'to_date': self.wp_street
}
# docids = self.env['sale.order'].search([]).ids
return self.env.ref('idg_bphtb.action_report_bphtb_sspd').report_action(self) # None, data=data
def _validate_wp_seller(self):
if self.wp_id == self.seller_id:
raise ValueError("Pembeli dan penjual harus berbeda")
# def create(self, vals, context=None):
# # Your code goes here
# res = super().create(vals, context=context) # 'uid', 'ids', and 'vals'
# self._validate_wp_seller()
# return res
#
# def write(self, vals, context=None):
# # Your code goes here
# res = super().write(vals, context=None)
# # Your code goes here
# self._validate_wp_seller()
# return res
#
def _get_sales_display_name(self, show_ref=False):
''' Helper to get the display name of an invoice depending of its type.
:param show_ref: A flag indicating of the display name must include or not the journal entry reference.
:return: A string representing the invoice.
'''
self.ensure_one()
draft_name = ''
if self.state == 'draft':
# draft_name += {
# 'out_invoice': _('Draft SSPD'),
# 'out_refund': _('Draft Credit Note'),
# 'in_invoice': _('Draft Bill'),
# 'in_refund': _('Draft Vendor Credit Note'),
# 'out_receipt': _('Draft Sales Receipt'),
# 'in_receipt': _('Draft Purchase Receipt'),
# 'entry': _('Draft Entry'),
# }[self.move_type]
if not self.name or self.name == '/':
draft_name += ' (* %s)' % str(self.id)
else:
draft_name += ' ' + self.name
return draft_name or self.name
# return draft_name or self.name + (show_ref and self.ref
# and ' (%s%s)' % (self.ref[:50],
# '...' if len(self.ref) > 50 else '') or '')
def _get_report_base_filename(self):
return self._get_sales_display_name()
def _get_last_sequence_domain(self, relaxed=False):
self.ensure_one()
if not self.date: # or not self.journal_id
return "WHERE FALSE", {}
where_string = "WHERE company_id= %(company_id)s AND typ = %(typ)s AND name != '/'"
param = {
'typ': self.typ,
'company_id': self.company_id.id
}
if not relaxed:
domain = [('typ', '=', self.typ), ('id', '!=', self.id or self._origin.id),
('name', 'not in', ('/', '', False))] #
reference_move_name = self.search(domain + [('date', '<=', self.date)], order='date desc', limit=1).name
if not reference_move_name:
reference_move_name = self.search(domain, order='date asc', limit=1).name
sequence_number_reset = self._deduce_sequence_number_reset(reference_move_name)
if sequence_number_reset == 'year':
where_string += " AND date_trunc('year', date::timestamp without time zone) = date_trunc('year', " \
"%(date)s) "
param['date'] = self.date
param['anti_regex'] = re.sub(r"\?P<\w+>", "?:", self._sequence_monthly_regex.split('(?P<seq>')[0]) + '$'
elif sequence_number_reset == 'month':
where_string += " AND date_trunc('month', date::timestamp without time zone) = date_trunc('month', " \
"%(date)s) "
param['date'] = self.date
else:
param['anti_regex'] = re.sub(r"\?P<\w+>", "?:", self._sequence_yearly_regex.split('(?P<seq>')[0]) + '$'
if param.get('anti_regex'): # and not self.journal_id.sequence_override_regex:
where_string += " AND sequence_prefix !~ %(anti_regex)s "
# if self.journal_id.refund_sequence:
# if self.move_type in ('out_refund', 'in_refund'):
# where_string += " AND move_type IN ('out_refund', 'in_refund') "
# else:
# where_string += " AND move_type NOT IN ('out_refund', 'in_refund') "
return where_string, param
def _get_starting_sequence(self):
self.ensure_one()
# self.typ.upper()
prefix1 = F"{self.typ.upper()}/{self.company_id.district_id.state_id.code}{self.company_id.district_id.code}"
starting_sequence = "%s/%04d/%02d/00000" % (prefix1, self.date.year, self.date.month)
# if self.journal_id.refund_sequence and self.move_type in ('out_refund', 'in_refund'):
# starting_sequence = "R" + starting_sequence
return starting_sequence
#Portal Area
def _compute_access_url(self):
super(BphtbSales, self)._compute_access_url()
for bphtb in self:
bphtb.access_url = '/my/bphtb/%s' % (bphtb.id)