diff --git a/netforce_clinic/layouts/clinic_cust_invoice_form.xml b/netforce_clinic/layouts/clinic_cust_invoice_form.xml index 175ea6c..b0c7f41 100644 --- a/netforce_clinic/layouts/clinic_cust_invoice_form.xml +++ b/netforce_clinic/layouts/clinic_cust_invoice_form.xml @@ -4,6 +4,6 @@ - + diff --git a/netforce_clinic/layouts/clinic_hd_case_form.xml b/netforce_clinic/layouts/clinic_hd_case_form.xml index 8826fa1..5c31cf4 100644 --- a/netforce_clinic/layouts/clinic_hd_case_form.xml +++ b/netforce_clinic/layouts/clinic_hd_case_form.xml @@ -57,6 +57,7 @@ + @@ -110,12 +111,9 @@ - - - - + - + diff --git a/netforce_clinic/models/account_invoice.py b/netforce_clinic/models/account_invoice.py index cb48057..49c8213 100644 --- a/netforce_clinic/models/account_invoice.py +++ b/netforce_clinic/models/account_invoice.py @@ -76,4 +76,208 @@ class AccountInvoice(Model): } get_model("account.fixed.asset").create(vals) + def post(self,ids,context={}): + t0=time.time() + settings=get_model("settings").browse(1) + for obj in self.browse(ids): + obj.check_related() + if abs(obj.amount_total)<0.001: + raise Exception("Invoice total is zero") + partner=obj.partner_id + if obj.type=="out": + account_id=partner.account_receivable_id.id or settings.account_receivable_id.id + if not account_id: + raise Exception("Account receivable not found") + elif obj.type=="in": + account_id=partner.account_payable_id.id or settings.account_payable_id.id + if not account_id: + raise Exception("Account payable not found") + sign=obj.type=="out" and 1 or -1 + if obj.inv_type=="credit": + sign*=-1 + #XXX ratchawat + if not obj.patient_partner_id: + obj.write({"account_id": account_id}) + if obj.type=="out": + desc="Sale; "+partner.name + elif obj.type=="in": + desc="Purchase; "+partner.name + if obj.type=="out": + journal_id=settings.sale_journal_id.id + if not journal_id: + raise Exception("Sales journal not found") + elif obj.type=="in": + journal_id=settings.purchase_journal_id.id + if not journal_id: + raise Exception("Purchases journal not found") + if obj.currency_rate: + currency_rate=obj.currency_rate + else: + if obj.currency_id.id==settings.currency_id.id: + currency_rate=1.0 + else: + rate_from=obj.currency_id.get_rate(date=obj.date) + if not rate_from: + raise Exception("Missing currency rate for %s"%obj.currency_id.code) + rate_to=settings.currency_id.get_rate(date=obj.date) + if not rate_to: + raise Exception("Missing currency rate for %s"%settings.currency_id.code) + currency_rate=rate_from/rate_to + obj.write({"currency_rate":currency_rate}) + move_vals={ + "journal_id": journal_id, + "number": obj.number, + "date": obj.date, + "ref": obj.ref, + "narration": desc, + "related_id": "account.invoice,%s"%obj.id, + "company_id": obj.company_id.id, + } + lines=[] + taxes={} + tax_nos=[] + t01=time.time() + total_amt=0.0 + total_base=0.0 + total_tax=0.0 + for line in obj.lines: + cur_amt=get_model("currency").convert(line.amount,obj.currency_id.id,settings.currency_id.id,rate=currency_rate) + total_amt+=cur_amt + tax_id=line.tax_id + if tax_id and obj.tax_type!="no_tax": + base_amt=get_model("account.tax.rate").compute_base(tax_id,cur_amt,tax_type=obj.tax_type) + tax_comps=get_model("account.tax.rate").compute_taxes(tax_id,base_amt,when="invoice") + for comp_id,tax_amt in tax_comps.items(): + tax_vals=taxes.setdefault(comp_id,{"tax_amt":0,"base_amt":0}) + tax_vals["tax_amt"]+=tax_amt + tax_vals["base_amt"]+=base_amt + total_tax+=tax_amt + else: + base_amt=cur_amt + total_base+=base_amt + acc_id=line.account_id.id + if not acc_id: + raise Exception("Missing line account for invoice line '%s'"%line.description) + amt=base_amt*sign + line_vals={ + "description": line.description, + "account_id": acc_id, + "credit": amt>0 and amt or 0, + "debit": amt<0 and -amt or 0, + "track_id": line.track_id.id, + "track2_id": line.track2_id.id, + "partner_id": partner.id, + } + lines.append(line_vals) + for comp_id,tax_vals in taxes.items(): + comp=get_model("account.tax.component").browse(comp_id) + acc_id=comp.account_id.id + if not acc_id: + raise Exception("Missing account for tax component %s"%comp.name) + amt=tax_vals["tax_amt"]*sign + line_vals={ + "description": desc, + "account_id": acc_id, + "credit": amt>0 and amt or 0, + "debit": amt<0 and -amt or 0, + "tax_comp_id": comp_id, + "tax_base": tax_vals["base_amt"], + "partner_id": partner.id, + "invoice_id": obj.id, + } + if comp.type in ("vat","vat_exempt"): + if obj.type=="out": + if obj.tax_no: + tax_no=obj.tax_no + else: + tax_no=self.gen_tax_no(exclude=tax_nos,context={"date":obj.date}) + tax_nos.append(tax_no) + obj.write({"tax_no":tax_no}) + line_vals["tax_no"]=tax_no + elif obj.type=="in": + line_vals["tax_no"]=obj.tax_no + lines.append(line_vals) + if obj.tax_type=="tax_in": + rounding=total_amt-(total_base+total_tax) + if abs(rounding)>0.00499: # XXX + amt=rounding*sign + if not settings.rounding_account_id.id: + raise Exception("Missing rounding account in financial settings") + line_vals={ + "description": desc, + "account_id": settings.rounding_account_id.id, + "credit": amt>0 and amt or 0, + "debit": amt<0 and -amt or 0, + "partner_id": partner.id, + "invoice_id": obj.id, + } + lines.append(line_vals) + t02=time.time() + dt01=(t02-t01)*1000 + print("post dt01",dt01) + groups={} + keys=["description","account_id","track_id","tax_comp_id","partner_id","invoice_id","reconcile_id"] + for line in lines: + key_val=tuple(line.get(k) for k in keys) + if key_val in groups: + group=groups[key_val] + group["debit"]+=line["debit"] + group["credit"]+=line["credit"] + if line.get("tax_base"): + if "tax_base" not in group: + group["tax_base"]=0 + group["tax_base"]+=line["tax_base"] + else: + groups[key_val]=line.copy() + group_lines=sorted(groups.values(),key=lambda l: (l["debit"],l["credit"])) + for line in group_lines: + amt=line["debit"]-line["credit"] + amt=get_model("currency").round(settings.currency_id.id,amt) + if amt>=0: + line["debit"]=amt + line["credit"]=0 + else: + line["debit"]=0 + line["credit"]=-amt + amt=0 + for line in group_lines: + amt-=line["debit"]-line["credit"] + line_vals={ + "description": desc, + "account_id": account_id, + "debit": amt>0 and amt or 0, + "credit": amt<0 and -amt or 0, + "due_date": obj.due_date, + "partner_id": partner.id, + } + + #XXX ratchawat: get receive account from invoice directly + if obj.patient_partner_id: + line_vals['account_id']=obj.account_id.id + + acc=get_model("account.account").browse(account_id) + if acc.currency_id.id!=settings.currency_id.id: + if acc.currency_id.id!=obj.currency_id.id: + raise Exception("Invalid account currency for this invoice: %s"%acc.code) + line_vals["amount_cur"]=obj.amount_total*sign + move_vals["lines"]=[("create",line_vals)] + move_vals["lines"]+=[("create",vals) for vals in group_lines] + t03=time.time() + dt02=(t03-t02)*1000 + print("post dt02",dt02) + move_id=get_model("account.move").create(move_vals) + t04=time.time() + dt03=(t04-t03)*1000 + print("post dt03",dt03) + get_model("account.move").post([move_id]) + t05=time.time() + dt04=(t05-t04)*1000 + print("post dt04",dt04) + obj.write({"move_id":move_id,"state":"waiting_payment"}) + t06=time.time() + dt05=(t06-t05)*1000 + print("post dt05",dt05) + t1=time.time() + dt=(t1-t0)*1000 + print("invoice.post <<< %d ms"%dt) AccountInvoice.register() diff --git a/netforce_clinic/models/hd_case.py b/netforce_clinic/models/hd_case.py index 8ad1c6b..987c088 100644 --- a/netforce_clinic/models/hd_case.py +++ b/netforce_clinic/models/hd_case.py @@ -635,7 +635,7 @@ class HDCase(Model): else: acc=prod_acc(prod.id,stype.id,'cash') account_id=acc.get("ar_credit_id",None) - ar_debit_id=acc.get("ar_debit_id",None) + ar_debit_id=acc.get("ar_debit_id",None) # account receiveable if not account_id: raise Exception("No Income Credit Account for product [%s] %s"%(prod.code, prod.name)) if not ar_debit_id: @@ -668,58 +668,78 @@ class HDCase(Model): patient=obj.patient_id patient_partner=patient.partner_id + + def group_invoice_line(invoice_lines): + invoice_vals={} + for mode,invoice_line in invoice_lines: + ar_debit_id=invoice_line['ar_debit_id'] + if not invoice_vals.get(ar_debit_id): + invoice_vals[ar_debit_id]=[] + del invoice_line['ar_debit_id'] + invoice_vals[ar_debit_id].append((mode,invoice_line)) + return invoice_vals if rmb_lines: ptype=patient.type_id partner=ptype.contact_id if not partner: raise Exception("No contact for patient type %s"%obj.ptype.name) - vals={ - "type": "out", - "inv_type": "invoice", - "tax_type": "tax_in", - 'date': obj.date, - 'due_date': due_date, - "ref": '%s (%s)'%(patient.name or '',patient.number or ''), - 'department_id': obj.department_id.id, - "related_id": "clinic.hd.case,%s"%obj.id, - "currency_id": currency_id, - "company_id": company_id, - "lines": [], - "company_id": company_id, - } - vals["partner_id"]=partner.id - vals['lines']=rmb_lines - if patient_partner: - vals['patient_partner_id']=patient_partner.id, + #vals['lines']=rmb_lines + # reset account receiveable + # group invoice line by account receiveable + invoices=group_invoice_line(rmb_lines) if obj.branch_id: context['branch_id']=obj.branch_id.id - get_model("account.invoice").create(vals,context=context) - + for account_receiveable_id, lines in invoices.items(): + vals={ + "type": "out", + "inv_type": "invoice", + "tax_type": "tax_in", + 'date': obj.date, + 'due_date': due_date, + "ref": '%s (%s)'%(patient.name or '',patient.number or ''), + 'department_id': obj.department_id.id, + "related_id": "clinic.hd.case,%s"%obj.id, + "currency_id": currency_id, + "company_id": company_id, + "lines": [], + "company_id": company_id, + "partner_id": partner.id, + 'patient_partner_id':patient_partner.id, + 'account_id':account_receiveable_id , + 'lines':lines, + } + inv_id=get_model("account.invoice").create(vals,context=context) + inv=get_model("account.invoice").browse(inv_id) + + # click button Credit from popup of button Pay if normb_lines and is_credit: partner=patient.partner_id if not partner: raise Exception("No contact for this patient %s"%obj.partner.name) context['branch_id']=obj.branch_id.id - number=self._get_number_invoice_noclaim(context=context) - vals={ - 'number': number, - "type": "out", - "inv_type": "invoice", - "tax_type": "tax_in", - 'due_date': due_date, - "ref": '%s (%s)'%(patient.name or '',patient.number or ''), - "related_id": "clinic.hd.case,%s"%obj.id, - "currency_id": currency_id, - "company_id": company_id, - "lines": [], - "company_id": company_id, - 'partner_id':partner.id, - } - vals['lines']=normb_lines - if patient_partner: - vals['patient_partner_id']=patient_partner.id, - get_model("account.invoice").create(vals,context) + + invoices=group_invoice_line(normb_lines) + for account_receiveable_id, lines in invoices.items(): + number=self._get_number_invoice_noclaim(context=context) + vals={ + 'number': number, + "type": "out", + "inv_type": "invoice", + "tax_type": "tax_in", + 'due_date': due_date, + "ref": '%s (%s)'%(patient.name or '',patient.number or ''), + "related_id": "clinic.hd.case,%s"%obj.id, + "currency_id": currency_id, + "company_id": company_id, + "lines": [], + "company_id": company_id, + 'partner_id':partner.id, + 'patient_partner_id':patient_partner.id, + 'account_id': account_receiveable_id, + } + vals['lines']=lines + get_model("account.invoice").create(vals,context) obj.make_pickings() # prevent douplicate create invoice & picking