mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 13:41:47 +00:00
Merge pull request #55327 from nabinhait/erpnext-refactoring
refactor: ERPNext file structure refactoring [WIP]
This commit is contained in:
@@ -60,7 +60,7 @@ frappe.ui.form.on("Dunning", {
|
||||
if (frm.doc.docstatus === 0) {
|
||||
frm.add_custom_button(__("Fetch Overdue Payments"), () => {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
method: "erpnext.accounts.doctype.sales_invoice.mapper.create_dunning",
|
||||
source_doctype: "Sales Invoice",
|
||||
date_field: "due_date",
|
||||
target: frm,
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe.utils import add_days, nowdate, today
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import (
|
||||
create_dunning as create_dunning_from_sales_invoice,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
|
||||
@@ -73,7 +73,7 @@ class TestDunning(ERPNextTestSuite):
|
||||
dunning = create_dunning_from_sales_invoice(si1.name)
|
||||
dunning.overdue_payments = []
|
||||
|
||||
method = "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning"
|
||||
method = "erpnext.accounts.doctype.sales_invoice.mapper.create_dunning"
|
||||
updated_dunning = mapper.map_docs(method, json.dumps([si1.name, si2.name]), dunning)
|
||||
|
||||
self.assertEqual(len(updated_dunning.overdue_payments), 2)
|
||||
|
||||
@@ -24,7 +24,6 @@ from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
get_stock_and_account_balance,
|
||||
@@ -1120,87 +1119,9 @@ class JournalEntry(AccountsController):
|
||||
self.total_amount_in_words = money_in_words(amt, currency)
|
||||
|
||||
def build_gl_map(self):
|
||||
gl_map = []
|
||||
from erpnext.accounts.doctype.journal_entry.services.gl_composer import JournalEntryGLComposer
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
self.transaction_currency = company_currency
|
||||
self.transaction_exchange_rate = 1
|
||||
if self.multi_currency:
|
||||
for row in self.get("accounts"):
|
||||
if row.account_currency != company_currency:
|
||||
# Journal assumes the first foreign currency as transaction currency
|
||||
self.transaction_currency = row.account_currency
|
||||
self.transaction_exchange_rate = row.exchange_rate
|
||||
break
|
||||
|
||||
advance_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
r = [x for x in r if x]
|
||||
remarks = "\n".join(r)
|
||||
|
||||
row = {
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
}
|
||||
|
||||
if d.reference_type in advance_doctypes:
|
||||
row.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_type,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
# set flag to skip party validation
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"] and self.party_not_required:
|
||||
frappe.flags.party_not_required = True
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
row,
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
return gl_map
|
||||
return JournalEntryGLComposer(self).compose()
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
103
erpnext/accounts/doctype/journal_entry/services/gl_composer.py
Normal file
103
erpnext/accounts/doctype/journal_entry/services/gl_composer.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.services.base_gl_composer import BaseGLComposer
|
||||
from erpnext.accounts.utils import get_advance_payment_doctypes
|
||||
|
||||
|
||||
class JournalEntryGLComposer(BaseGLComposer):
|
||||
"""Assembles the GL entries for a Journal Entry.
|
||||
|
||||
A Journal Entry already carries its ledger rows in the ``accounts`` child
|
||||
table, so composing is a straight projection of those rows into GL dicts
|
||||
via ``self.get_gl_dict``. The transaction currency/rate are resolved
|
||||
from the first foreign-currency row (mirroring the former build_gl_map).
|
||||
"""
|
||||
|
||||
def compose(self):
|
||||
doc = self.doc
|
||||
gl_map = []
|
||||
|
||||
company_currency = erpnext.get_company_currency(doc.company)
|
||||
doc.transaction_currency = company_currency
|
||||
doc.transaction_exchange_rate = 1
|
||||
if doc.multi_currency:
|
||||
for row in doc.get("accounts"):
|
||||
if row.account_currency != company_currency:
|
||||
# Journal assumes the first foreign currency as transaction currency
|
||||
doc.transaction_currency = row.account_currency
|
||||
doc.transaction_exchange_rate = row.exchange_rate
|
||||
break
|
||||
|
||||
advance_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
for d in doc.get("accounts"):
|
||||
if d.debit or d.credit or (doc.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, doc.remark]
|
||||
r = [x for x in r if x]
|
||||
remarks = "\n".join(r)
|
||||
|
||||
row = {
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": doc.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": doc.transaction_currency,
|
||||
"transaction_exchange_rate": doc.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if doc.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / doc.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if doc.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / doc.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": doc.finance_book,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
}
|
||||
|
||||
if d.reference_type in advance_doctypes:
|
||||
row.update(
|
||||
{
|
||||
"against_voucher_type": doc.doctype,
|
||||
"against_voucher": doc.name,
|
||||
"advance_voucher_type": d.reference_type,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
# set flag to skip party validation
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"] and doc.party_not_required:
|
||||
frappe.flags.party_not_required = True
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
row,
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
return gl_map
|
||||
@@ -1287,17 +1287,9 @@ class PaymentEntry(AccountsController):
|
||||
self.transaction_exchange_rate = self.target_exchange_rate
|
||||
|
||||
def build_gl_map(self):
|
||||
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
||||
self.setup_party_account_field()
|
||||
self.set_transaction_currency_and_rate()
|
||||
from erpnext.accounts.doctype.payment_entry.services.gl_composer import PaymentEntryGLComposer
|
||||
|
||||
gl_entries = []
|
||||
self.add_party_gl_entries(gl_entries)
|
||||
self.add_bank_gl_entries(gl_entries)
|
||||
self.add_deductions_gl_entries(gl_entries)
|
||||
self.add_tax_gl_entries(gl_entries)
|
||||
add_regional_gl_entries(gl_entries, self)
|
||||
return gl_entries
|
||||
return PaymentEntryGLComposer(self).compose()
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gl_entries = self.build_gl_map()
|
||||
@@ -1313,132 +1305,6 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
self.make_advance_gl_entries(cancel=cancel)
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
|
||||
party_account_type = frappe.db.get_value("Party Type", self.party_type, "account_type")
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
if (
|
||||
d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
and d.allocated_amount < 0
|
||||
and (
|
||||
(party_account_type == "Receivable" and self.payment_type == "Pay")
|
||||
or (party_account_type == "Payable" and self.payment_type == "Receive")
|
||||
)
|
||||
):
|
||||
# reversing dr_cr because because it will get reversed in gl processing due to negative amount
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": cost_center,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_transaction_currency": d.allocated_amount
|
||||
if self.transaction_currency == self.party_account_currency
|
||||
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
"transaction_exchange_rate": self.target_exchange_rate,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# advance reference
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_doctype,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
elif self.book_advance_payments_in_separate_party_account:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
else:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
gle.update(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr + "_in_transaction_currency": self.unallocated_amount
|
||||
if self.party_account_currency == self.transaction_currency
|
||||
else base_unallocated_amount / self.transaction_exchange_rate,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
):
|
||||
@@ -1560,132 +1426,6 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def add_bank_gl_entries(self, gl_entries):
|
||||
if self.payment_type in ("Pay", "Internal Transfer"):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.paid_from,
|
||||
"account_currency": self.paid_from_account_currency,
|
||||
"against": self.party if self.payment_type == "Pay" else self.paid_to,
|
||||
"credit_in_account_currency": self.paid_amount,
|
||||
"credit_in_transaction_currency": self.paid_amount
|
||||
if self.paid_from_account_currency == self.transaction_currency
|
||||
else self.base_paid_amount / self.transaction_exchange_rate,
|
||||
"credit": self.base_paid_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
if self.payment_type in ("Receive", "Internal Transfer"):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.paid_to,
|
||||
"account_currency": self.paid_to_account_currency,
|
||||
"against": self.party if self.payment_type == "Receive" else self.paid_from,
|
||||
"debit_in_account_currency": self.received_amount,
|
||||
"debit_in_transaction_currency": self.received_amount
|
||||
if self.paid_to_account_currency == self.transaction_currency
|
||||
else self.base_received_amount / self.transaction_exchange_rate,
|
||||
"debit": self.base_received_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def add_tax_gl_entries(self, gl_entries):
|
||||
for d in self.get("taxes"):
|
||||
account_currency = get_account_currency(d.account_head)
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
|
||||
|
||||
if self.payment_type in ("Pay", "Internal Transfer"):
|
||||
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
|
||||
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
|
||||
against = self.party or self.paid_from
|
||||
elif self.payment_type == "Receive":
|
||||
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
|
||||
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
|
||||
against = self.party or self.paid_to
|
||||
|
||||
payment_account = self.get_party_account_for_taxes()
|
||||
tax_amount = d.tax_amount
|
||||
base_tax_amount = d.base_tax_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": d.account_head,
|
||||
"against": against,
|
||||
dr_or_cr: tax_amount,
|
||||
dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency == self.company_currency
|
||||
else d.tax_amount,
|
||||
dr_or_cr + "_in_transaction_currency": base_tax_amount
|
||||
/ self.transaction_exchange_rate,
|
||||
"cost_center": d.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
account_currency,
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
|
||||
if not d.included_in_paid_amount:
|
||||
if get_account_currency(payment_account) != self.company_currency:
|
||||
if self.payment_type == "Receive":
|
||||
exchange_rate = self.target_exchange_rate
|
||||
elif self.payment_type in ["Pay", "Internal Transfer"]:
|
||||
exchange_rate = self.source_exchange_rate
|
||||
base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount"))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": payment_account,
|
||||
"against": against,
|
||||
rev_dr_or_cr: tax_amount,
|
||||
rev_dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency == self.company_currency
|
||||
else d.tax_amount,
|
||||
rev_dr_or_cr + "_in_transaction_currency": base_tax_amount
|
||||
/ self.transaction_exchange_rate,
|
||||
"cost_center": self.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
account_currency,
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
|
||||
def add_deductions_gl_entries(self, gl_entries):
|
||||
for d in self.get("deductions"):
|
||||
if not d.amount:
|
||||
continue
|
||||
|
||||
account_currency = get_account_currency(d.account)
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": d.account,
|
||||
"account_currency": account_currency,
|
||||
"against": self.party or self.paid_from,
|
||||
"debit_in_account_currency": d.amount,
|
||||
"debit_in_transaction_currency": d.amount / self.transaction_exchange_rate,
|
||||
"debit": d.amount,
|
||||
"cost_center": d.cost_center,
|
||||
},
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
|
||||
def get_party_account_for_taxes(self):
|
||||
if self.payment_type == "Receive":
|
||||
return self.paid_to
|
||||
|
||||
293
erpnext/accounts/doctype/payment_entry/services/gl_composer.py
Normal file
293
erpnext/accounts/doctype/payment_entry/services/gl_composer.py
Normal file
@@ -0,0 +1,293 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.services.base_gl_composer import BaseGLComposer
|
||||
from erpnext.accounts.utils import get_account_currency, get_advance_payment_doctypes
|
||||
|
||||
|
||||
class PaymentEntryGLComposer(BaseGLComposer):
|
||||
"""Assembles the GL entries for a Payment Entry.
|
||||
|
||||
The voucher-specific row builders live here and operate on ``self.doc``.
|
||||
Shared helpers (get_gl_dict, calculate_base_allocated_amount_for_reference,
|
||||
get_exchange_rate, get_party_account_for_taxes) remain on the document for
|
||||
now and are invoked via ``self.doc``. The advance-posting builders stay on
|
||||
the document; they post separately from this compose pass and move with the
|
||||
advances service in a later phase.
|
||||
"""
|
||||
|
||||
def compose(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import add_regional_gl_entries
|
||||
|
||||
doc = self.doc
|
||||
if doc.payment_type in ("Receive", "Pay") and not doc.get("party_account_field"):
|
||||
doc.setup_party_account_field()
|
||||
doc.set_transaction_currency_and_rate()
|
||||
|
||||
gl_entries = []
|
||||
self.add_party_gl_entries(gl_entries)
|
||||
self.add_bank_gl_entries(gl_entries)
|
||||
self.add_deductions_gl_entries(gl_entries)
|
||||
self.add_tax_gl_entries(gl_entries)
|
||||
add_regional_gl_entries(gl_entries, doc)
|
||||
return gl_entries
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
if not doc.party_account:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
if doc.payment_type == "Receive":
|
||||
against_account = doc.paid_to
|
||||
else:
|
||||
against_account = doc.paid_from
|
||||
|
||||
party_account_type = frappe.db.get_value("Party Type", doc.party_type, "account_type")
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": doc.party_account,
|
||||
"party_type": doc.party_type,
|
||||
"party": doc.party,
|
||||
"against": against_account,
|
||||
"account_currency": doc.party_account_currency,
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
|
||||
for d in doc.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if doc.payment_type == "Receive" else "debit"
|
||||
cost_center = doc.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = doc.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
if (
|
||||
d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
and d.allocated_amount < 0
|
||||
and (
|
||||
(party_account_type == "Receivable" and doc.payment_type == "Pay")
|
||||
or (party_account_type == "Payable" and doc.payment_type == "Receive")
|
||||
)
|
||||
):
|
||||
# reversing dr_cr because because it will get reversed in gl processing due to negative amount
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.party_account,
|
||||
"party_type": doc.party_type,
|
||||
"party": doc.party,
|
||||
"against": against_account,
|
||||
"account_currency": doc.party_account_currency,
|
||||
"cost_center": cost_center,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_transaction_currency": d.allocated_amount
|
||||
if doc.transaction_currency == doc.party_account_currency
|
||||
else allocated_amount_in_company_currency / doc.transaction_exchange_rate,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
"transaction_exchange_rate": doc.target_exchange_rate,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# advance reference
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": doc.doctype,
|
||||
"against_voucher": doc.name,
|
||||
"advance_voucher_type": d.reference_doctype,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
elif doc.book_advance_payments_in_separate_party_account:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": doc.doctype, "against_voucher": doc.name})
|
||||
else:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
gl_entries.append(gle)
|
||||
|
||||
if doc.unallocated_amount:
|
||||
dr_or_cr = "credit" if doc.payment_type == "Receive" else "debit"
|
||||
exchange_rate = doc.get_exchange_rate()
|
||||
base_unallocated_amount = doc.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
gle.update(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.party_account,
|
||||
"party_type": doc.party_type,
|
||||
"party": doc.party,
|
||||
"against": against_account,
|
||||
"account_currency": doc.party_account_currency,
|
||||
"cost_center": doc.cost_center,
|
||||
dr_or_cr + "_in_account_currency": doc.unallocated_amount,
|
||||
dr_or_cr + "_in_transaction_currency": doc.unallocated_amount
|
||||
if doc.party_account_currency == doc.transaction_currency
|
||||
else base_unallocated_amount / doc.transaction_exchange_rate,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
if doc.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": doc.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def add_bank_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
if doc.payment_type in ("Pay", "Internal Transfer"):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.paid_from,
|
||||
"account_currency": doc.paid_from_account_currency,
|
||||
"against": doc.party if doc.payment_type == "Pay" else doc.paid_to,
|
||||
"credit_in_account_currency": doc.paid_amount,
|
||||
"credit_in_transaction_currency": doc.paid_amount
|
||||
if doc.paid_from_account_currency == doc.transaction_currency
|
||||
else doc.base_paid_amount / doc.transaction_exchange_rate,
|
||||
"credit": doc.base_paid_amount,
|
||||
"cost_center": doc.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
if doc.payment_type in ("Receive", "Internal Transfer"):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.paid_to,
|
||||
"account_currency": doc.paid_to_account_currency,
|
||||
"against": doc.party if doc.payment_type == "Receive" else doc.paid_from,
|
||||
"debit_in_account_currency": doc.received_amount,
|
||||
"debit_in_transaction_currency": doc.received_amount
|
||||
if doc.paid_to_account_currency == doc.transaction_currency
|
||||
else doc.base_received_amount / doc.transaction_exchange_rate,
|
||||
"debit": doc.base_received_amount,
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def add_tax_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
for d in doc.get("taxes"):
|
||||
account_currency = get_account_currency(d.account_head)
|
||||
if account_currency != doc.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, doc.company_currency))
|
||||
|
||||
if doc.payment_type in ("Pay", "Internal Transfer"):
|
||||
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
|
||||
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
|
||||
against = doc.party or doc.paid_from
|
||||
elif doc.payment_type == "Receive":
|
||||
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
|
||||
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
|
||||
against = doc.party or doc.paid_to
|
||||
|
||||
payment_account = doc.get_party_account_for_taxes()
|
||||
tax_amount = d.tax_amount
|
||||
base_tax_amount = d.base_tax_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": d.account_head,
|
||||
"against": against,
|
||||
dr_or_cr: tax_amount,
|
||||
dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency == doc.company_currency
|
||||
else d.tax_amount,
|
||||
dr_or_cr + "_in_transaction_currency": base_tax_amount
|
||||
/ doc.transaction_exchange_rate,
|
||||
"cost_center": d.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
account_currency,
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
|
||||
if not d.included_in_paid_amount:
|
||||
if get_account_currency(payment_account) != doc.company_currency:
|
||||
if doc.payment_type == "Receive":
|
||||
exchange_rate = doc.target_exchange_rate
|
||||
elif doc.payment_type in ["Pay", "Internal Transfer"]:
|
||||
exchange_rate = doc.source_exchange_rate
|
||||
base_tax_amount = flt((tax_amount / exchange_rate), doc.precision("paid_amount"))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": payment_account,
|
||||
"against": against,
|
||||
rev_dr_or_cr: tax_amount,
|
||||
rev_dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency == doc.company_currency
|
||||
else d.tax_amount,
|
||||
rev_dr_or_cr + "_in_transaction_currency": base_tax_amount
|
||||
/ doc.transaction_exchange_rate,
|
||||
"cost_center": doc.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
account_currency,
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
|
||||
def add_deductions_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
for d in doc.get("deductions"):
|
||||
if not d.amount:
|
||||
continue
|
||||
|
||||
account_currency = get_account_currency(d.account)
|
||||
if account_currency != doc.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account, doc.company_currency))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": d.account,
|
||||
"account_currency": account_currency,
|
||||
"against": doc.party or doc.paid_from,
|
||||
"debit_in_account_currency": d.amount,
|
||||
"debit_in_transaction_currency": d.amount / doc.transaction_exchange_rate,
|
||||
"debit": d.amount,
|
||||
"cost_center": d.cost_center,
|
||||
},
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
@@ -196,7 +196,7 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
self.assertEqual(outstanding_amount, 100)
|
||||
|
||||
def test_reference_outstanding_amount_on_advance_pull(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
|
||||
|
||||
so = make_sales_order(qty=1, rate=1000)
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
@@ -1567,7 +1567,7 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
self.check_pl_entries()
|
||||
|
||||
def test_advance_as_liability_against_order(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
from erpnext.buying.doctype.purchase_order.mapper import (
|
||||
make_purchase_invoice as _make_purchase_invoice,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
@@ -15,13 +15,13 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_any_doc_running,
|
||||
)
|
||||
from erpnext.accounts.services.advances import get_advance_payment_entries_for_regional
|
||||
from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
create_gain_loss_journal,
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional
|
||||
|
||||
|
||||
class PaymentReconciliation(Document):
|
||||
|
||||
@@ -443,7 +443,7 @@ class PaymentRequest(Document):
|
||||
self.update_reference_advance_payment_status()
|
||||
|
||||
def make_invoice(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
|
||||
|
||||
si = make_sales_invoice(self.reference_name, ignore_permissions=True)
|
||||
si.allocate_advances_automatically = True
|
||||
|
||||
@@ -330,7 +330,7 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
Test Sales Invoice and Return Sales Invoice creation during POS Invoice mode.
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
get_mode_of_payment_info,
|
||||
update_multi_mode_option,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.services.loyalty import LoyaltyService
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.controllers.queries import item_query as _item_query
|
||||
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
|
||||
@@ -241,13 +242,13 @@ class POSInvoice(SalesInvoice):
|
||||
def on_submit(self):
|
||||
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
|
||||
if not self.is_return and self.loyalty_program:
|
||||
self.make_loyalty_point_entry()
|
||||
LoyaltyService(self).make_loyalty_point_entry()
|
||||
elif self.is_return and self.return_against and self.loyalty_program:
|
||||
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
|
||||
against_psi_doc.delete_loyalty_point_entry()
|
||||
against_psi_doc.make_loyalty_point_entry()
|
||||
LoyaltyService(against_psi_doc).delete_loyalty_point_entry()
|
||||
LoyaltyService(against_psi_doc).make_loyalty_point_entry()
|
||||
if self.redeem_loyalty_points and self.loyalty_points:
|
||||
self.apply_loyalty_points()
|
||||
LoyaltyService(self).apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
@@ -288,11 +289,11 @@ class POSInvoice(SalesInvoice):
|
||||
# run on cancel method of selling controller
|
||||
super(SalesInvoice, self).on_cancel()
|
||||
if not self.is_return and self.loyalty_program:
|
||||
self.delete_loyalty_point_entry()
|
||||
LoyaltyService(self).delete_loyalty_point_entry()
|
||||
elif self.is_return and self.return_against and self.loyalty_program:
|
||||
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
|
||||
against_psi_doc.delete_loyalty_point_entry()
|
||||
against_psi_doc.make_loyalty_point_entry()
|
||||
LoyaltyService(against_psi_doc).delete_loyalty_point_entry()
|
||||
LoyaltyService(against_psi_doc).make_loyalty_point_entry()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
@@ -745,7 +746,9 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
# fetch charges
|
||||
if self.taxes_and_charges and not len(self.get("taxes")):
|
||||
self.set_taxes()
|
||||
from erpnext.accounts.services.taxes import TaxService
|
||||
|
||||
TaxService(self).set_taxes()
|
||||
|
||||
if not self.account_for_change_amount:
|
||||
self.account_for_change_amount = frappe.get_cached_value(
|
||||
|
||||
129
erpnext/accounts/doctype/purchase_invoice/mapper.py
Normal file
129
erpnext/accounts/doctype/purchase_invoice/mapper.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.accounts_controller import merge_taxes
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_debit_note(source_name: str, target_doc: str | Document | None = None):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
return make_return_doc("Purchase Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(source_name: str, target_doc: str | Document | None = None):
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Invoice",
|
||||
source_name,
|
||||
{
|
||||
"Purchase Invoice": {"doctype": "Stock Entry", "validation": {"docstatus": ["=", 1]}},
|
||||
"Purchase Invoice Item": {
|
||||
"doctype": "Stock Entry Detail",
|
||||
"field_map": {"stock_qty": "transfer_qty", "batch_no": "batch_no"},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_inter_company_sales_invoice(source_name: str, target_doc: Document | None = None):
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_transaction
|
||||
|
||||
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(
|
||||
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
|
||||
):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
remove_items_with_zero_qty(target_parent)
|
||||
set_missing_values(source_parent, target_parent)
|
||||
|
||||
def remove_items_with_zero_qty(target_parent):
|
||||
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
|
||||
|
||||
def set_missing_values(source_parent, target_parent):
|
||||
target_parent.run_method("set_missing_values")
|
||||
if args and args.get("merge_taxes"):
|
||||
merge_taxes(source_parent, target_parent)
|
||||
target_parent.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
|
||||
|
||||
returned_qty_map = (
|
||||
get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, obj.name, "Purchase Invoice"
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
|
||||
obj.conversion_factor
|
||||
)
|
||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||
target.base_amount = (
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Invoice",
|
||||
source_name,
|
||||
{
|
||||
"Purchase Invoice": {
|
||||
"doctype": "Purchase Receipt",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Invoice Item": {
|
||||
"doctype": "Purchase Receipt Item",
|
||||
"field_map": {
|
||||
"name": "purchase_invoice_item",
|
||||
"parent": "purchase_invoice",
|
||||
"bom": "bom",
|
||||
"purchase_order": "purchase_order",
|
||||
"po_detail": "purchase_order_item",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"reset_value": not (args and args.get("merge_taxes")),
|
||||
"ignore": args.get("merge_taxes") if args else 0,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
post_parent_process,
|
||||
)
|
||||
|
||||
return doc
|
||||
@@ -156,7 +156,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
__("Purchase Order"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice",
|
||||
method: "erpnext.buying.doctype.purchase_order.mapper.make_purchase_invoice",
|
||||
source_doctype: "Purchase Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
@@ -181,7 +181,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
__("Purchase Receipt"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
|
||||
method: "erpnext.stock.doctype.purchase_receipt.mapper.make_purchase_invoice",
|
||||
source_doctype: "Purchase Receipt",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
@@ -414,7 +414,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
make_inter_company_invoice(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_inter_company_sales_invoice",
|
||||
method: "erpnext.accounts.doctype.purchase_invoice.mapper.make_inter_company_sales_invoice",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
@@ -474,7 +474,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
make_debit_note() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_debit_note",
|
||||
method: "erpnext.accounts.doctype.purchase_invoice.mapper.make_debit_note",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
@@ -720,7 +720,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
|
||||
make_purchase_receipt: function (frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
|
||||
method: "erpnext.accounts.doctype.purchase_invoice.mapper.make_purchase_receipt",
|
||||
frm: frm,
|
||||
freeze_message: __("Creating Purchase Receipt ..."),
|
||||
});
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||
|
||||
@@ -36,7 +33,7 @@ from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
update_billed_amount_based_on_po,
|
||||
@@ -292,7 +289,10 @@ class PurchaseInvoice(BuyingController):
|
||||
self.set_against_expense_account()
|
||||
self.validate_write_off_account()
|
||||
self.validate_write_off_cost_center()
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
|
||||
from erpnext.accounts.services.billing_validation import BillingValidationService
|
||||
|
||||
BillingValidationService(self).validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
validate_inter_company_party(
|
||||
@@ -875,34 +875,11 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
def get_gl_entries(self, inventory_account_map=None):
|
||||
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
||||
from erpnext.accounts.doctype.purchase_invoice.services.gl_composer import (
|
||||
PurchaseInvoiceGLComposer,
|
||||
)
|
||||
|
||||
if self.auto_accounting_for_stock:
|
||||
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
|
||||
else:
|
||||
self.stock_received_but_not_billed = None
|
||||
|
||||
self.negative_expense_to_be_booked = 0.0
|
||||
gl_entries = []
|
||||
|
||||
self.make_supplier_gl_entry(gl_entries)
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
self.make_gl_entries_for_tax_withholding(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
|
||||
gl_entries = merge_similar_entries(gl_entries)
|
||||
|
||||
self.make_payment_gl_entries(gl_entries)
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
self.set_gl_entry_for_purchase_expense(gl_entries)
|
||||
return gl_entries
|
||||
return PurchaseInvoiceGLComposer(self).compose(inventory_account_map)
|
||||
|
||||
def check_asset_cwip_enabled(self):
|
||||
# Check if there exists any item with cwip accounting enabled in it's asset category
|
||||
@@ -913,788 +890,6 @@ class PurchaseInvoice(BuyingController):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def make_supplier_gl_entry(self, gl_entries):
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introduction of posting GLE based on rounded total
|
||||
grand_total = (
|
||||
self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||
)
|
||||
base_grand_total = flt(
|
||||
self.base_rounded_total
|
||||
if (self.base_rounding_adjustment and self.base_rounded_total)
|
||||
else self.base_grand_total,
|
||||
self.precision("base_grand_total"),
|
||||
)
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
|
||||
|
||||
def add_supplier_gl_entry(
|
||||
self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False
|
||||
):
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl = {
|
||||
"account": self.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"due_date": self.due_date,
|
||||
"against": against_account or self.against_expense_account,
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"credit_in_transaction_currency": grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
"cost_center": self.cost_center,
|
||||
"_skip_merge": skip_merge,
|
||||
}
|
||||
|
||||
if remarks:
|
||||
gl["remarks"] = remarks
|
||||
|
||||
gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self))
|
||||
|
||||
def make_item_gl_entries(self, gl_entries):
|
||||
# item gl entries
|
||||
stock_items = self.get_stock_items()
|
||||
if self.update_stock and self.auto_accounting_for_stock:
|
||||
inventory_account_map = self.get_inventory_account_map()
|
||||
|
||||
landed_cost_entries = self.get_item_account_wise_lcv_entries()
|
||||
|
||||
voucher_wise_stock_value = {}
|
||||
if self.update_stock:
|
||||
stock_ledger_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["voucher_detail_no", "stock_value_difference", "warehouse"],
|
||||
filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0},
|
||||
)
|
||||
for d in stock_ledger_entries:
|
||||
voucher_wise_stock_value.setdefault(
|
||||
(d.voucher_detail_no, d.warehouse), d.stock_value_difference
|
||||
)
|
||||
|
||||
valuation_tax_accounts = [
|
||||
d.account_head
|
||||
for d in self.get("taxes")
|
||||
if d.category in ("Valuation", "Valuation and Total")
|
||||
and flt(d.base_tax_amount_after_discount_amount)
|
||||
]
|
||||
|
||||
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
|
||||
|
||||
provisional_accounting_for_non_stock_items = cint(
|
||||
frappe.get_cached_value(
|
||||
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
||||
)
|
||||
)
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.get_provisional_accounts()
|
||||
|
||||
adjust_incoming_rate = frappe.db.get_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||
)
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
if (
|
||||
self.update_stock
|
||||
and self.auto_accounting_for_stock
|
||||
and (item.item_code in stock_items or item.is_fixed_asset)
|
||||
):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
# warehouse account
|
||||
warehouse_debit_amount = self.make_stock_adjustment_entry(
|
||||
gl_entries, item, voucher_wise_stock_value, account_currency
|
||||
)
|
||||
|
||||
if item.from_warehouse:
|
||||
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
|
||||
|
||||
_inv_dict_from_warehouse = self.get_inventory_account_dict(
|
||||
item, inventory_account_map, "from_warehouse"
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": _inv_dict["account"],
|
||||
"against": _inv_dict_from_warehouse["account"],
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": warehouse_debit_amount,
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
_inv_dict["account_currency"],
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
credit_amount = item.base_net_amount
|
||||
if self.is_internal_supplier and item.valuation_rate:
|
||||
credit_amount = flt(item.valuation_rate * item.stock_qty)
|
||||
|
||||
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": _inv_dict_from_warehouse["account"],
|
||||
"against": _inv_dict["account"],
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
_inv_dict_from_warehouse["account_currency"],
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# Do not book expense for transfer within same company transfer
|
||||
if not self.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
if not self.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": warehouse_debit_amount,
|
||||
"debit_in_transaction_currency": flt(
|
||||
warehouse_debit_amount / self.conversion_rate,
|
||||
item.precision("net_amount"),
|
||||
),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# Amount added through landed-cost-voucher
|
||||
if landed_cost_entries:
|
||||
if (item.item_code, item.name) in landed_cost_entries:
|
||||
for account, base_amount in landed_cost_entries[
|
||||
(item.item_code, item.name)
|
||||
].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(base_amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(base_amount["amount"]),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# sub-contracting warehouse
|
||||
if flt(item.rm_supp_cost):
|
||||
supplier_wh_dict = self.get_inventory_account_dict(
|
||||
item, inventory_account_map, "supplier_warehouse"
|
||||
)
|
||||
|
||||
supplier_inventory_account = supplier_wh_dict["account"]
|
||||
if not supplier_inventory_account:
|
||||
frappe.throw(
|
||||
_("Please set account in Warehouse {0}").format(self.supplier_warehouse)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": supplier_inventory_account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(item.rm_supp_cost),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
supplier_wh_dict["account_currency"],
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
expense_account = (
|
||||
item.expense_account
|
||||
if (not item.enable_deferred_expense or self.is_return)
|
||||
else item.deferred_expense_account
|
||||
)
|
||||
|
||||
account_currency = get_account_currency(expense_account)
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, None)
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.make_provisional_gl_entry(gl_entries, item)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": base_amount,
|
||||
"debit_in_transaction_currency": amount,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# check if the exchange rate has changed
|
||||
if (
|
||||
not adjust_incoming_rate
|
||||
and item.get("purchase_receipt")
|
||||
and self.auto_accounting_for_stock
|
||||
):
|
||||
if (
|
||||
exchange_rate_map[item.purchase_receipt]
|
||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||
and item.net_rate == net_rate_map[item.pr_detail]
|
||||
and item.item_code in stock_items
|
||||
):
|
||||
discrepancy_caused_by_exchange_rate_difference = (
|
||||
item.qty * item.net_rate
|
||||
) * (exchange_rate_map[item.purchase_receipt] - self.conversion_rate)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": discrepancy_caused_by_exchange_rate_difference,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.get_company_default("exchange_gain_loss_account"),
|
||||
"against": self.supplier,
|
||||
"credit": discrepancy_caused_by_exchange_rate_difference,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
self.auto_accounting_for_stock
|
||||
and self.is_opening == "No"
|
||||
and item.item_code in stock_items
|
||||
and item.item_tax_amount
|
||||
):
|
||||
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
|
||||
if item.purchase_receipt and valuation_tax_accounts:
|
||||
negative_expense_booked_in_pr = frappe.db.sql(
|
||||
"""select name from `tabGL Entry`
|
||||
where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""",
|
||||
(item.purchase_receipt, valuation_tax_accounts),
|
||||
)
|
||||
|
||||
(
|
||||
self.get_company_default("asset_received_but_not_billed")
|
||||
if item.is_fixed_asset
|
||||
else self.stock_received_but_not_billed
|
||||
)
|
||||
|
||||
if not negative_expense_booked_in_pr:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.stock_received_but_not_billed,
|
||||
"against": self.supplier,
|
||||
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
|
||||
"debit_in_transaction_currency": flt(
|
||||
item.item_tax_amount / self.conversion_rate,
|
||||
item.precision("item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
"cost_center": self.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
self.negative_expense_to_be_booked += flt(
|
||||
item.item_tax_amount, item.precision("item_tax_amount")
|
||||
)
|
||||
|
||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||
self.update_net_purchase_amount_for_linked_assets(item)
|
||||
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
|
||||
if not linked_purchase_receipts:
|
||||
return
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
default_provisional_account = self.get_company_default("default_provisional_account")
|
||||
provisional_accounts = set(
|
||||
[
|
||||
d.provisional_expense_account
|
||||
if d.provisional_expense_account
|
||||
else default_provisional_account
|
||||
for d in pr_items
|
||||
]
|
||||
)
|
||||
|
||||
provisional_gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": ("in", linked_purchase_receipts),
|
||||
"account": ("in", provisional_accounts),
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
fields=["voucher_detail_no"],
|
||||
)
|
||||
rows_with_provisional_entries = [d.voucher_detail_no for d in provisional_gl_entries]
|
||||
for item in pr_items:
|
||||
self.provisional_accounts[item.name] = {
|
||||
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||
"qty": item.qty,
|
||||
"base_rate": item.base_rate,
|
||||
"rate": item.rate,
|
||||
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||
}
|
||||
|
||||
def make_provisional_gl_entry(self, gl_entries, item):
|
||||
if item.purchase_receipt:
|
||||
pr_item = self.provisional_accounts.get(item.pr_detail, {})
|
||||
if pr_item.get("has_provisional_entry"):
|
||||
purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt)
|
||||
|
||||
# Intentionally passing purchase invoice item to handle partial billing
|
||||
purchase_receipt_doc.add_provisional_gl_entry(
|
||||
item,
|
||||
gl_entries,
|
||||
self.posting_date,
|
||||
pr_item.get("provisional_account"),
|
||||
reverse=1,
|
||||
item_amount=(
|
||||
(min(item.qty, pr_item.get("qty")) * pr_item.get("rate"))
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
|
||||
def update_net_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={
|
||||
"purchase_invoice": self.name,
|
||||
"item_code": item.item_code,
|
||||
"purchase_invoice_item": ("in", [item.name, ""]),
|
||||
},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
|
||||
frappe.db.set_value(
|
||||
"Asset",
|
||||
asset.name,
|
||||
{
|
||||
"net_purchase_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
|
||||
net_amt_precision = item.precision("base_net_amount")
|
||||
val_rate_db_precision = 6 if cint(item.precision("valuation_rate")) <= 6 else 9
|
||||
|
||||
warehouse_debit_amount = flt(
|
||||
flt(item.valuation_rate, val_rate_db_precision) * flt(item.qty) * flt(item.conversion_factor),
|
||||
net_amt_precision,
|
||||
)
|
||||
|
||||
if self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
warehouse_debit_amount = flt(
|
||||
voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
elif (
|
||||
self.update_stock
|
||||
and voucher_wise_stock_value.get((item.name, item.warehouse))
|
||||
and warehouse_debit_amount
|
||||
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
||||
):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
||||
stock_adjustment_amt = warehouse_debit_amount - stock_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
# tax table gl entries
|
||||
valuation_tax = {}
|
||||
|
||||
for tax in self.get("taxes"):
|
||||
amount, base_amount = self.get_tax_amounts(tax, None)
|
||||
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
|
||||
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": tax.account_head,
|
||||
"against": self.supplier,
|
||||
dr_or_cr: base_amount,
|
||||
dr_or_cr + "_in_account_currency": base_amount
|
||||
if account_currency == self.company_currency
|
||||
else amount,
|
||||
dr_or_cr + "_in_transaction_currency": amount,
|
||||
"cost_center": tax.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
item=tax,
|
||||
)
|
||||
)
|
||||
# accumulate valuation tax
|
||||
if (
|
||||
self.is_opening == "No"
|
||||
and tax.category in ("Valuation", "Valuation and Total")
|
||||
and flt(base_amount)
|
||||
and not self.is_internal_transfer()
|
||||
):
|
||||
if self.auto_accounting_for_stock and not tax.cost_center:
|
||||
frappe.throw(
|
||||
_("Cost Center is required in row {0} in Taxes table for type {1}").format(
|
||||
tax.idx, _(tax.category)
|
||||
)
|
||||
)
|
||||
valuation_tax.setdefault(tax.name, 0)
|
||||
valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
|
||||
|
||||
if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax:
|
||||
# credit valuation tax amount in "Expenses Included In Valuation"
|
||||
# this will balance out valuation amount included in cost of goods sold
|
||||
|
||||
total_valuation_amount = sum(valuation_tax.values())
|
||||
amount_including_divisional_loss = self.negative_expense_to_be_booked
|
||||
i = 1
|
||||
for tax in self.get("taxes"):
|
||||
if valuation_tax.get(tax.name):
|
||||
if i == len(valuation_tax):
|
||||
applicable_amount = amount_including_divisional_loss
|
||||
else:
|
||||
applicable_amount = self.negative_expense_to_be_booked * (
|
||||
valuation_tax[tax.name] / total_valuation_amount
|
||||
)
|
||||
amount_including_divisional_loss -= applicable_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": tax.account_head,
|
||||
"cost_center": tax.cost_center,
|
||||
"against": self.supplier,
|
||||
"credit": applicable_amount,
|
||||
"credit_in_transaction_currency": flt(
|
||||
applicable_amount / self.conversion_rate,
|
||||
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
},
|
||||
item=tax,
|
||||
)
|
||||
)
|
||||
|
||||
i += 1
|
||||
|
||||
if self.auto_accounting_for_stock and self.update_stock and valuation_tax:
|
||||
for tax in self.get("taxes"):
|
||||
if valuation_tax.get(tax.name):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": tax.account_head,
|
||||
"cost_center": tax.cost_center,
|
||||
"against": self.supplier,
|
||||
"credit": valuation_tax[tax.name],
|
||||
"credit_in_transaction_currency": flt(
|
||||
valuation_tax[tax.name] / self.conversion_rate,
|
||||
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
},
|
||||
item=tax,
|
||||
)
|
||||
)
|
||||
|
||||
def make_internal_transfer_gl_entries(self, gl_entries):
|
||||
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
|
||||
account_currency = get_account_currency(self.unrealized_profit_loss_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.unrealized_profit_loss_account,
|
||||
"against": self.supplier,
|
||||
"credit": flt(self.total_taxes_and_charges),
|
||||
"credit_in_transaction_currency": flt(self.total_taxes_and_charges),
|
||||
"credit_in_account_currency": flt(self.base_total_taxes_and_charges),
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def make_gl_entries_for_tax_withholding(self, gl_entries):
|
||||
"""
|
||||
Tax withholding amount is not part of supplier invoice.
|
||||
Separate supplier GL Entry for correct reporting.
|
||||
"""
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
for row in self.get("taxes"):
|
||||
if not row.is_tax_withholding_account or not row.tax_amount:
|
||||
continue
|
||||
|
||||
base_tds_amount = row.base_tax_amount_after_discount_amount
|
||||
tds_amount = row.tax_amount_after_discount_amount
|
||||
|
||||
self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount)
|
||||
self.add_supplier_gl_entry(
|
||||
gl_entries,
|
||||
-base_tds_amount,
|
||||
-tds_amount,
|
||||
against_account=row.account_head,
|
||||
remarks=_("TDS Deducted"),
|
||||
skip_merge=True,
|
||||
)
|
||||
|
||||
def make_payment_gl_entries(self, gl_entries):
|
||||
# Make Cash GL Entries
|
||||
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
bank_account_currency = get_account_currency(self.cash_bank_account)
|
||||
# CASH, make payment entries
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"against": self.cash_bank_account,
|
||||
"debit": self.base_paid_amount,
|
||||
"debit_in_account_currency": self.base_paid_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else self.paid_amount,
|
||||
"debit_in_transaction_currency": self.paid_amount,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
},
|
||||
self.party_account_currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.cash_bank_account,
|
||||
"against": self.supplier,
|
||||
"credit": self.base_paid_amount,
|
||||
"credit_in_account_currency": self.base_paid_amount
|
||||
if bank_account_currency == self.company_currency
|
||||
else self.paid_amount,
|
||||
"credit_in_transaction_currency": self.paid_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
bank_account_currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def make_write_off_gl_entry(self, gl_entries):
|
||||
# writeoff account includes petty difference in the invoice amount
|
||||
# and the amount that is paid
|
||||
if self.write_off_account and flt(self.write_off_amount):
|
||||
write_off_account_currency = get_account_currency(self.write_off_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"against": self.write_off_account,
|
||||
"debit": self.base_write_off_amount,
|
||||
"debit_in_account_currency": self.base_write_off_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else self.write_off_amount,
|
||||
"debit_in_transaction_currency": self.write_off_amount,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
},
|
||||
self.party_account_currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.write_off_account,
|
||||
"against": self.supplier,
|
||||
"credit": flt(self.base_write_off_amount),
|
||||
"credit_in_account_currency": self.base_write_off_amount
|
||||
if write_off_account_currency == self.company_currency
|
||||
else self.write_off_amount,
|
||||
"credit_in_transaction_currency": self.write_off_amount,
|
||||
"cost_center": self.cost_center or self.write_off_cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def make_gle_for_rounding_adjustment(self, gl_entries):
|
||||
# if rounding adjustment in small and conversion rate is also small then
|
||||
# base_rounding_adjustment may become zero due to small precision
|
||||
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
|
||||
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry
|
||||
if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
if self.is_opening == "Yes" and self.rounding_adjustment:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||
).format(
|
||||
frappe.bold(self.rounding_adjustment),
|
||||
frappe.bold("Round Off for Opening"),
|
||||
get_link_to_form("Company", self.company),
|
||||
frappe.bold("Disable Rounded Total"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
round_off_account = round_off_for_opening
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": self.supplier,
|
||||
"debit_in_account_currency": self.rounding_adjustment,
|
||||
"debit": self.base_rounding_adjustment,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else (self.cost_center or round_off_cost_center),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
check_if_return_invoice_linked_with_payment_entry(self)
|
||||
|
||||
@@ -1964,31 +1159,6 @@ def make_regional_gl_entries(gl_entries, doc):
|
||||
return gl_entries
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_debit_note(source_name: str, target_doc: str | Document | None = None):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
return make_return_doc("Purchase Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(source_name: str, target_doc: str | Document | None = None):
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Invoice",
|
||||
source_name,
|
||||
{
|
||||
"Purchase Invoice": {"doctype": "Stock Entry", "validation": {"docstatus": ["=", 1]}},
|
||||
"Purchase Invoice Item": {
|
||||
"doctype": "Stock Entry Detail",
|
||||
"field_map": {"stock_qty": "transfer_qty", "batch_no": "batch_no"},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def change_release_date(name: str, release_date: str | None = None):
|
||||
if frappe.db.exists("Purchase Invoice", name):
|
||||
@@ -2008,95 +1178,3 @@ def block_invoice(name: str, release_date: str, hold_comment: str | None = None)
|
||||
if frappe.db.exists("Purchase Invoice", name):
|
||||
pi = frappe.get_lazy_doc("Purchase Invoice", name)
|
||||
pi.block_invoice(hold_comment, release_date)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_inter_company_sales_invoice(source_name: str, target_doc: Document | None = None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
|
||||
|
||||
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(
|
||||
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
|
||||
):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
remove_items_with_zero_qty(target_parent)
|
||||
set_missing_values(source_parent, target_parent)
|
||||
|
||||
def remove_items_with_zero_qty(target_parent):
|
||||
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
|
||||
|
||||
def set_missing_values(source_parent, target_parent):
|
||||
target_parent.run_method("set_missing_values")
|
||||
if args and args.get("merge_taxes"):
|
||||
merge_taxes(source_parent, target_parent)
|
||||
target_parent.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
|
||||
|
||||
returned_qty_map = (
|
||||
get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, obj.name, "Purchase Invoice"
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
|
||||
obj.conversion_factor
|
||||
)
|
||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||
target.base_amount = (
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Invoice",
|
||||
source_name,
|
||||
{
|
||||
"Purchase Invoice": {
|
||||
"doctype": "Purchase Receipt",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Invoice Item": {
|
||||
"doctype": "Purchase Receipt Item",
|
||||
"field_map": {
|
||||
"name": "purchase_invoice_item",
|
||||
"parent": "purchase_invoice",
|
||||
"bom": "bom",
|
||||
"purchase_order": "purchase_order",
|
||||
"po_detail": "purchase_order_item",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"reset_value": not (args and args.get("merge_taxes")),
|
||||
"ignore": args.get("merge_taxes") if args else 0,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
post_parent_process,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -0,0 +1,850 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.services.base_gl_composer import BaseGLComposer
|
||||
from erpnext.accounts.services.taxes import TaxService
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
|
||||
|
||||
class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
"""Assembles the GL entries for a Purchase Invoice."""
|
||||
|
||||
def compose(self, inventory_account_map=None):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_regional_gl_entries
|
||||
from erpnext.accounts.general_ledger import merge_similar_entries
|
||||
|
||||
doc = self.doc
|
||||
doc.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(doc.company)
|
||||
|
||||
if doc.auto_accounting_for_stock:
|
||||
doc.stock_received_but_not_billed = doc.get_company_default("stock_received_but_not_billed")
|
||||
else:
|
||||
doc.stock_received_but_not_billed = None
|
||||
|
||||
doc.negative_expense_to_be_booked = 0.0
|
||||
gl_entries = []
|
||||
|
||||
self.make_supplier_gl_entry(gl_entries)
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
self.make_gl_entries_for_tax_withholding(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, doc)
|
||||
gl_entries = merge_similar_entries(gl_entries)
|
||||
|
||||
self.make_payment_gl_entries(gl_entries)
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
doc.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
doc.set_gl_entry_for_purchase_expense(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
doc = self.doc
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
_round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
doc.company, "Purchase Invoice", doc.name, doc.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
precision_loss = doc.get("base_net_total") - flt(
|
||||
doc.get("net_total") * doc.conversion_rate, doc.precision("net_total")
|
||||
)
|
||||
|
||||
if precision_loss:
|
||||
gl_entries.append(
|
||||
doc.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": doc.supplier,
|
||||
"credit": precision_loss,
|
||||
"cost_center": round_off_cost_center
|
||||
if doc.use_company_roundoff_cost_center
|
||||
else doc.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def make_supplier_gl_entry(self, gl_entries):
|
||||
doc = self.doc
|
||||
grand_total = (
|
||||
doc.rounded_total if (doc.rounding_adjustment and doc.rounded_total) else doc.grand_total
|
||||
)
|
||||
base_grand_total = flt(
|
||||
doc.base_rounded_total
|
||||
if (doc.base_rounding_adjustment and doc.base_rounded_total)
|
||||
else doc.base_grand_total,
|
||||
doc.precision("base_grand_total"),
|
||||
)
|
||||
if grand_total and not doc.is_internal_transfer():
|
||||
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
|
||||
|
||||
def add_supplier_gl_entry(
|
||||
self,
|
||||
gl_entries,
|
||||
base_grand_total,
|
||||
grand_total,
|
||||
against_account=None,
|
||||
remarks=None,
|
||||
skip_merge=False,
|
||||
):
|
||||
doc = self.doc
|
||||
against_voucher = doc.name
|
||||
if doc.is_return and doc.return_against and not doc.update_outstanding_for_self:
|
||||
against_voucher = doc.return_against
|
||||
|
||||
gl = {
|
||||
"account": doc.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": doc.supplier,
|
||||
"due_date": doc.due_date,
|
||||
"against": against_account or doc.against_expense_account,
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if doc.party_account_currency == doc.company_currency
|
||||
else grand_total,
|
||||
"credit_in_transaction_currency": grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": doc.doctype,
|
||||
"project": doc.project,
|
||||
"cost_center": doc.cost_center,
|
||||
"_skip_merge": skip_merge,
|
||||
}
|
||||
if remarks:
|
||||
gl["remarks"] = remarks
|
||||
gl_entries.append(self.get_gl_dict(gl, doc.party_account_currency, item=doc))
|
||||
|
||||
def make_item_gl_entries(self, gl_entries):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||
get_purchase_document_details,
|
||||
)
|
||||
|
||||
doc = self.doc
|
||||
tax_service = TaxService(doc)
|
||||
stock_items = doc.get_stock_items()
|
||||
if doc.update_stock and doc.auto_accounting_for_stock:
|
||||
inventory_account_map = doc.get_inventory_account_map()
|
||||
|
||||
landed_cost_entries = doc.get_item_account_wise_lcv_entries()
|
||||
|
||||
voucher_wise_stock_value = {}
|
||||
if doc.update_stock:
|
||||
stock_ledger_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["voucher_detail_no", "stock_value_difference", "warehouse"],
|
||||
filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0},
|
||||
)
|
||||
for d in stock_ledger_entries:
|
||||
voucher_wise_stock_value.setdefault(
|
||||
(d.voucher_detail_no, d.warehouse), d.stock_value_difference
|
||||
)
|
||||
|
||||
valuation_tax_accounts = [
|
||||
d.account_head
|
||||
for d in doc.get("taxes")
|
||||
if d.category in ("Valuation", "Valuation and Total")
|
||||
and flt(d.base_tax_amount_after_discount_amount)
|
||||
]
|
||||
|
||||
exchange_rate_map, net_rate_map = get_purchase_document_details(doc)
|
||||
|
||||
provisional_accounting_for_non_stock_items = cint(
|
||||
frappe.get_cached_value(
|
||||
"Company", doc.company, "enable_provisional_accounting_for_non_stock_items"
|
||||
)
|
||||
)
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.get_provisional_accounts()
|
||||
|
||||
adjust_incoming_rate = frappe.db.get_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||
)
|
||||
|
||||
for item in doc.get("items"):
|
||||
if flt(item.base_net_amount) or (doc.get("update_stock") and item.valuation_rate):
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
if (
|
||||
doc.update_stock
|
||||
and doc.auto_accounting_for_stock
|
||||
and (item.item_code in stock_items or item.is_fixed_asset)
|
||||
):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
warehouse_debit_amount = self.make_stock_adjustment_entry(
|
||||
gl_entries, item, voucher_wise_stock_value, account_currency
|
||||
)
|
||||
|
||||
if item.from_warehouse:
|
||||
_inv_dict = doc.get_inventory_account_dict(item, inventory_account_map)
|
||||
_inv_dict_from_warehouse = doc.get_inventory_account_dict(
|
||||
item, inventory_account_map, "from_warehouse"
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": _inv_dict["account"],
|
||||
"against": _inv_dict_from_warehouse["account"],
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": warehouse_debit_amount,
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
_inv_dict["account_currency"],
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
credit_amount = item.base_net_amount
|
||||
if doc.is_internal_supplier and item.valuation_rate:
|
||||
credit_amount = flt(item.valuation_rate * item.stock_qty)
|
||||
|
||||
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": _inv_dict_from_warehouse["account"],
|
||||
"against": _inv_dict["account"],
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
_inv_dict_from_warehouse["account_currency"],
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
if not doc.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account,
|
||||
"against": doc.supplier,
|
||||
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
if not doc.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account,
|
||||
"against": doc.supplier,
|
||||
"debit": warehouse_debit_amount,
|
||||
"debit_in_transaction_currency": flt(
|
||||
warehouse_debit_amount / doc.conversion_rate,
|
||||
item.precision("net_amount"),
|
||||
),
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# Amount added through landed-cost-voucher
|
||||
if landed_cost_entries:
|
||||
if (item.item_code, item.name) in landed_cost_entries:
|
||||
for account, base_amount in landed_cost_entries[
|
||||
(item.item_code, item.name)
|
||||
].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(base_amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(base_amount["amount"]),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# sub-contracting warehouse
|
||||
if flt(item.rm_supp_cost):
|
||||
supplier_wh_dict = doc.get_inventory_account_dict(
|
||||
item, inventory_account_map, "supplier_warehouse"
|
||||
)
|
||||
supplier_inventory_account = supplier_wh_dict["account"]
|
||||
if not supplier_inventory_account:
|
||||
frappe.throw(
|
||||
_("Please set account in Warehouse {0}").format(doc.supplier_warehouse)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": supplier_inventory_account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(item.rm_supp_cost),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
supplier_wh_dict["account_currency"],
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
expense_account = (
|
||||
item.expense_account
|
||||
if (not item.enable_deferred_expense or doc.is_return)
|
||||
else item.deferred_expense_account
|
||||
)
|
||||
account_currency = get_account_currency(expense_account)
|
||||
amount, base_amount = tax_service.get_amount_and_base_amount(item, None)
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.make_provisional_gl_entry(gl_entries, item)
|
||||
|
||||
if not doc.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": doc.supplier,
|
||||
"debit": base_amount,
|
||||
"debit_in_transaction_currency": amount,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# check if the exchange rate has changed
|
||||
if (
|
||||
not adjust_incoming_rate
|
||||
and item.get("purchase_receipt")
|
||||
and doc.auto_accounting_for_stock
|
||||
):
|
||||
if (
|
||||
exchange_rate_map[item.purchase_receipt]
|
||||
and doc.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||
and item.net_rate == net_rate_map[item.pr_detail]
|
||||
and item.item_code in stock_items
|
||||
):
|
||||
discrepancy_caused_by_exchange_rate_difference = (
|
||||
item.qty * item.net_rate
|
||||
) * (exchange_rate_map[item.purchase_receipt] - doc.conversion_rate)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": doc.supplier,
|
||||
"debit": discrepancy_caused_by_exchange_rate_difference,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.get_company_default("exchange_gain_loss_account"),
|
||||
"against": doc.supplier,
|
||||
"credit": discrepancy_caused_by_exchange_rate_difference,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
doc.auto_accounting_for_stock
|
||||
and doc.is_opening == "No"
|
||||
and item.item_code in stock_items
|
||||
and item.item_tax_amount
|
||||
):
|
||||
# Post reverse entry for Stock-Received-But-Not-Billed if booked in Purchase Receipt
|
||||
if item.purchase_receipt and valuation_tax_accounts:
|
||||
negative_expense_booked_in_pr = frappe.db.sql(
|
||||
"""select name from `tabGL Entry`
|
||||
where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""",
|
||||
(item.purchase_receipt, valuation_tax_accounts),
|
||||
)
|
||||
|
||||
(
|
||||
doc.get_company_default("asset_received_but_not_billed")
|
||||
if item.is_fixed_asset
|
||||
else doc.stock_received_but_not_billed
|
||||
)
|
||||
|
||||
if not negative_expense_booked_in_pr:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.stock_received_but_not_billed,
|
||||
"against": doc.supplier,
|
||||
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
|
||||
"debit_in_transaction_currency": flt(
|
||||
item.item_tax_amount / doc.conversion_rate,
|
||||
item.precision("item_tax_amount"),
|
||||
),
|
||||
"remarks": doc.remarks or _("Accounting Entry for Stock"),
|
||||
"cost_center": doc.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
doc.negative_expense_to_be_booked += flt(
|
||||
item.item_tax_amount, item.precision("item_tax_amount")
|
||||
)
|
||||
|
||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||
self.update_net_purchase_amount_for_linked_assets(item)
|
||||
|
||||
def get_provisional_accounts(self):
|
||||
doc = self.doc
|
||||
self.provisional_accounts = frappe._dict()
|
||||
linked_purchase_receipts = {d.purchase_receipt for d in doc.items if d.purchase_receipt}
|
||||
if not linked_purchase_receipts:
|
||||
return
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
default_provisional_account = doc.get_company_default("default_provisional_account")
|
||||
provisional_accounts = {
|
||||
d.provisional_expense_account if d.provisional_expense_account else default_provisional_account
|
||||
for d in pr_items
|
||||
}
|
||||
|
||||
provisional_gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": ("in", linked_purchase_receipts),
|
||||
"account": ("in", provisional_accounts),
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
fields=["voucher_detail_no"],
|
||||
)
|
||||
rows_with_provisional_entries = [d.voucher_detail_no for d in provisional_gl_entries]
|
||||
for item in pr_items:
|
||||
self.provisional_accounts[item.name] = {
|
||||
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||
"qty": item.qty,
|
||||
"base_rate": item.base_rate,
|
||||
"rate": item.rate,
|
||||
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||
}
|
||||
|
||||
def make_provisional_gl_entry(self, gl_entries, item):
|
||||
if item.purchase_receipt:
|
||||
pr_item = self.provisional_accounts.get(item.pr_detail, {})
|
||||
if pr_item.get("has_provisional_entry"):
|
||||
purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt)
|
||||
|
||||
# Intentionally passing purchase invoice item to handle partial billing
|
||||
purchase_receipt_doc.add_provisional_gl_entry(
|
||||
item,
|
||||
gl_entries,
|
||||
self.doc.posting_date,
|
||||
pr_item.get("provisional_account"),
|
||||
reverse=1,
|
||||
item_amount=(
|
||||
(min(item.qty, pr_item.get("qty")) * pr_item.get("rate"))
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
|
||||
def update_net_purchase_amount_for_linked_assets(self, item):
|
||||
doc = self.doc
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={
|
||||
"purchase_invoice": doc.name,
|
||||
"item_code": item.item_code,
|
||||
"purchase_invoice_item": ("in", [item.name, ""]),
|
||||
},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
|
||||
frappe.db.set_value(
|
||||
"Asset",
|
||||
asset.name,
|
||||
{
|
||||
"net_purchase_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
|
||||
doc = self.doc
|
||||
net_amt_precision = item.precision("base_net_amount")
|
||||
val_rate_db_precision = 6 if cint(item.precision("valuation_rate")) <= 6 else 9
|
||||
|
||||
warehouse_debit_amount = flt(
|
||||
flt(item.valuation_rate, val_rate_db_precision) * flt(item.qty) * flt(item.conversion_factor),
|
||||
net_amt_precision,
|
||||
)
|
||||
|
||||
if doc.is_return and doc.update_stock and (doc.is_internal_supplier or not doc.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate:
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
warehouse_debit_amount = flt(
|
||||
voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = doc.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / doc.conversion_rate,
|
||||
"remarks": doc.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
elif (
|
||||
doc.update_stock
|
||||
and voucher_wise_stock_value.get((item.name, item.warehouse))
|
||||
and warehouse_debit_amount
|
||||
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
||||
):
|
||||
cost_of_goods_sold_account = doc.get_company_default("default_expense_account")
|
||||
stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
||||
stock_adjustment_amt = warehouse_debit_amount - stock_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / doc.conversion_rate,
|
||||
"remarks": doc.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
tax_service = TaxService(doc)
|
||||
valuation_tax = {}
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
amount, base_amount = tax_service.get_tax_amounts(tax, None)
|
||||
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": tax.account_head,
|
||||
"against": doc.supplier,
|
||||
dr_or_cr: base_amount,
|
||||
dr_or_cr + "_in_account_currency": base_amount
|
||||
if account_currency == doc.company_currency
|
||||
else amount,
|
||||
dr_or_cr + "_in_transaction_currency": amount,
|
||||
"cost_center": tax.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
item=tax,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
doc.is_opening == "No"
|
||||
and tax.category in ("Valuation", "Valuation and Total")
|
||||
and flt(base_amount)
|
||||
and not doc.is_internal_transfer()
|
||||
):
|
||||
if doc.auto_accounting_for_stock and not tax.cost_center:
|
||||
frappe.throw(
|
||||
_("Cost Center is required in row {0} in Taxes table for type {1}").format(
|
||||
tax.idx, _(tax.category)
|
||||
)
|
||||
)
|
||||
valuation_tax.setdefault(tax.name, 0)
|
||||
valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
|
||||
|
||||
if doc.is_opening == "No" and doc.negative_expense_to_be_booked and valuation_tax:
|
||||
total_valuation_amount = sum(valuation_tax.values())
|
||||
amount_including_divisional_loss = doc.negative_expense_to_be_booked
|
||||
i = 1
|
||||
for tax in doc.get("taxes"):
|
||||
if valuation_tax.get(tax.name):
|
||||
if i == len(valuation_tax):
|
||||
applicable_amount = amount_including_divisional_loss
|
||||
else:
|
||||
applicable_amount = doc.negative_expense_to_be_booked * (
|
||||
valuation_tax[tax.name] / total_valuation_amount
|
||||
)
|
||||
amount_including_divisional_loss -= applicable_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": tax.account_head,
|
||||
"cost_center": tax.cost_center,
|
||||
"against": doc.supplier,
|
||||
"credit": applicable_amount,
|
||||
"credit_in_transaction_currency": flt(
|
||||
applicable_amount / doc.conversion_rate,
|
||||
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
|
||||
),
|
||||
"remarks": doc.remarks or _("Accounting Entry for Stock"),
|
||||
},
|
||||
item=tax,
|
||||
)
|
||||
)
|
||||
i += 1
|
||||
|
||||
if doc.auto_accounting_for_stock and doc.update_stock and valuation_tax:
|
||||
for tax in doc.get("taxes"):
|
||||
if valuation_tax.get(tax.name):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": tax.account_head,
|
||||
"cost_center": tax.cost_center,
|
||||
"against": doc.supplier,
|
||||
"credit": valuation_tax[tax.name],
|
||||
"credit_in_transaction_currency": flt(
|
||||
valuation_tax[tax.name] / doc.conversion_rate,
|
||||
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
|
||||
),
|
||||
"remarks": doc.remarks or _("Accounting Entry for Stock"),
|
||||
},
|
||||
item=tax,
|
||||
)
|
||||
)
|
||||
|
||||
def make_internal_transfer_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
if doc.is_internal_transfer() and flt(doc.base_total_taxes_and_charges):
|
||||
account_currency = get_account_currency(doc.unrealized_profit_loss_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.unrealized_profit_loss_account,
|
||||
"against": doc.supplier,
|
||||
"credit": flt(doc.total_taxes_and_charges),
|
||||
"credit_in_transaction_currency": flt(doc.total_taxes_and_charges),
|
||||
"credit_in_account_currency": flt(doc.base_total_taxes_and_charges),
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def make_gl_entries_for_tax_withholding(self, gl_entries):
|
||||
"""Separate supplier GL entry for tax withholding (TDS) — not part of the supplier invoice amount."""
|
||||
doc = self.doc
|
||||
if not doc.apply_tds:
|
||||
return
|
||||
|
||||
for row in doc.get("taxes"):
|
||||
if not row.is_tax_withholding_account or not row.tax_amount:
|
||||
continue
|
||||
|
||||
base_tds_amount = row.base_tax_amount_after_discount_amount
|
||||
tds_amount = row.tax_amount_after_discount_amount
|
||||
|
||||
self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount)
|
||||
self.add_supplier_gl_entry(
|
||||
gl_entries,
|
||||
-base_tds_amount,
|
||||
-tds_amount,
|
||||
against_account=row.account_head,
|
||||
remarks=_("TDS Deducted"),
|
||||
skip_merge=True,
|
||||
)
|
||||
|
||||
def make_payment_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
if cint(doc.is_paid) and doc.cash_bank_account and doc.paid_amount:
|
||||
against_voucher = doc.name
|
||||
if doc.is_return and doc.return_against and not doc.update_outstanding_for_self:
|
||||
against_voucher = doc.return_against
|
||||
bank_account_currency = get_account_currency(doc.cash_bank_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": doc.supplier,
|
||||
"against": doc.cash_bank_account,
|
||||
"debit": doc.base_paid_amount,
|
||||
"debit_in_account_currency": doc.base_paid_amount
|
||||
if doc.party_account_currency == doc.company_currency
|
||||
else doc.paid_amount,
|
||||
"debit_in_transaction_currency": doc.paid_amount,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": doc.doctype,
|
||||
"cost_center": doc.cost_center,
|
||||
"project": doc.project,
|
||||
},
|
||||
doc.party_account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.cash_bank_account,
|
||||
"against": doc.supplier,
|
||||
"credit": doc.base_paid_amount,
|
||||
"credit_in_account_currency": doc.base_paid_amount
|
||||
if bank_account_currency == doc.company_currency
|
||||
else doc.paid_amount,
|
||||
"credit_in_transaction_currency": doc.paid_amount,
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
bank_account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def make_write_off_gl_entry(self, gl_entries):
|
||||
doc = self.doc
|
||||
if doc.write_off_account and flt(doc.write_off_amount):
|
||||
write_off_account_currency = get_account_currency(doc.write_off_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": doc.supplier,
|
||||
"against": doc.write_off_account,
|
||||
"debit": doc.base_write_off_amount,
|
||||
"debit_in_account_currency": doc.base_write_off_amount
|
||||
if doc.party_account_currency == doc.company_currency
|
||||
else doc.write_off_amount,
|
||||
"debit_in_transaction_currency": doc.write_off_amount,
|
||||
"against_voucher": doc.return_against
|
||||
if cint(doc.is_return) and doc.return_against
|
||||
else doc.name,
|
||||
"against_voucher_type": doc.doctype,
|
||||
"cost_center": doc.cost_center,
|
||||
"project": doc.project,
|
||||
},
|
||||
doc.party_account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.write_off_account,
|
||||
"against": doc.supplier,
|
||||
"credit": flt(doc.base_write_off_amount),
|
||||
"credit_in_account_currency": doc.base_write_off_amount
|
||||
if write_off_account_currency == doc.company_currency
|
||||
else doc.write_off_amount,
|
||||
"credit_in_transaction_currency": doc.write_off_amount,
|
||||
"cost_center": doc.cost_center or doc.write_off_cost_center,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def make_gle_for_rounding_adjustment(self, gl_entries):
|
||||
doc = self.doc
|
||||
if not doc.is_internal_transfer() and doc.rounding_adjustment and doc.base_rounding_adjustment:
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
doc.company, "Purchase Invoice", doc.name, doc.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
if doc.is_opening == "Yes" and doc.rounding_adjustment:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||
).format(
|
||||
frappe.bold(doc.rounding_adjustment),
|
||||
frappe.bold("Round Off for Opening"),
|
||||
get_link_to_form("Company", doc.company),
|
||||
frappe.bold("Disable Rounded Total"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
round_off_account = round_off_for_opening
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": doc.supplier,
|
||||
"debit_in_account_currency": doc.rounding_adjustment,
|
||||
"debit": doc.base_rounding_adjustment,
|
||||
"cost_center": round_off_cost_center
|
||||
if doc.use_company_roundoff_cost_center
|
||||
else (doc.cost_center or round_off_cost_center),
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
@@ -8,8 +8,8 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice as make_pi_from_po
|
||||
from erpnext.buying.doctype.purchase_order.mapper import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice as make_pi_from_po
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
@@ -20,9 +20,9 @@ from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.mapper import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.test_material_request import make_material_request
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
@@ -80,7 +80,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
pi.delete()
|
||||
|
||||
def test_update_received_qty_in_material_request(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice
|
||||
|
||||
"""
|
||||
Test if the received_qty in Material Request is updated correctly when
|
||||
@@ -346,7 +346,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
|
||||
)
|
||||
def test_purchase_invoice_with_exchange_rate_difference(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
)
|
||||
|
||||
@@ -388,7 +388,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
)
|
||||
|
||||
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
)
|
||||
|
||||
@@ -2162,7 +2162,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
@@ -2748,10 +2748,10 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
def test_invoice_against_returned_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_invoice as make_purchase_invoice_from_pr,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_return_against_rejected_warehouse,
|
||||
)
|
||||
|
||||
@@ -2892,7 +2892,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
self.assertEqual(invoice.grand_total, 300)
|
||||
|
||||
def test_pr_pi_over_billing(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_invoice as make_purchase_invoice_from_pr,
|
||||
)
|
||||
|
||||
@@ -2940,7 +2940,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
self.assertEqual(pi.discount_amount, discount_amount)
|
||||
|
||||
def test_returned_item_purchase_receipt(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||
from erpnext.accounts.doctype.purchase_invoice.mapper import (
|
||||
make_purchase_receipt as make_purchase_receipt_from_pi,
|
||||
)
|
||||
|
||||
|
||||
615
erpnext/accounts/doctype/sales_invoice/mapper.py
Normal file
615
erpnext/accounts/doctype/sales_invoice/mapper.py
Normal file
@@ -0,0 +1,615 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.utils import flt, get_link_to_form, getdate
|
||||
|
||||
from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, _get_party_details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_maintenance_schedule(source_name: str, target_doc: str | Document | None = None):
|
||||
doclist = get_mapped_doc(
|
||||
"Sales Invoice",
|
||||
source_name,
|
||||
{
|
||||
"Sales Invoice": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}},
|
||||
"Sales Invoice Item": {
|
||||
"doctype": "Maintenance Schedule Item",
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_delivery_note(source_name: str, target_doc: Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("set_po_nos")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
target_doc.qty = flt(source_doc.qty) - flt(source_doc.delivered_qty)
|
||||
target_doc.stock_qty = target_doc.qty * flt(source_doc.conversion_factor)
|
||||
|
||||
target_doc.base_amount = target_doc.qty * flt(source_doc.base_rate)
|
||||
target_doc.amount = target_doc.qty * flt(source_doc.rate)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Sales Invoice",
|
||||
source_name,
|
||||
{
|
||||
"Sales Invoice": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}},
|
||||
"Sales Invoice Item": {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
"name": "si_detail",
|
||||
"parent": "against_sales_invoice",
|
||||
"serial_no": "serial_no",
|
||||
"sales_order": "against_sales_order",
|
||||
"so_detail": "so_detail",
|
||||
"cost_center": "cost_center",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1
|
||||
and not doc.scio_detail
|
||||
and not doc.dn_detail
|
||||
and doc.qty - doc.delivered_qty > 0,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {
|
||||
"doctype": "Sales Team",
|
||||
"field_map": {"incentives": "incentives"},
|
||||
"add_if_empty": True,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_return(source_name: str, target_doc: Document | None = None):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
return make_return_doc("Sales Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
def get_inter_company_details(doc, doctype):
|
||||
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
|
||||
parties = frappe.db.get_all(
|
||||
"Supplier",
|
||||
fields=["name"],
|
||||
filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company},
|
||||
)
|
||||
company = frappe.get_cached_value("Customer", doc.customer, "represents_company")
|
||||
|
||||
if not parties:
|
||||
frappe.throw(
|
||||
_("No Supplier found for Inter Company Transactions which represents company {0}").format(
|
||||
frappe.bold(doc.company)
|
||||
)
|
||||
)
|
||||
|
||||
party = get_internal_party(parties, "Supplier", doc)
|
||||
else:
|
||||
parties = frappe.db.get_all(
|
||||
"Customer",
|
||||
fields=["name"],
|
||||
filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company},
|
||||
)
|
||||
company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company")
|
||||
|
||||
if not parties:
|
||||
frappe.throw(
|
||||
_("No Customer found for Inter Company Transactions which represents company {0}").format(
|
||||
frappe.bold(doc.company)
|
||||
)
|
||||
)
|
||||
|
||||
party = get_internal_party(parties, "Customer", doc)
|
||||
|
||||
return {"party": party, "company": company}
|
||||
|
||||
|
||||
def get_internal_party(parties, link_doctype, doc):
|
||||
if len(parties) == 1:
|
||||
party = parties[0].name
|
||||
else:
|
||||
# If more than one Internal Supplier/Customer, get supplier/customer on basis of address
|
||||
if doc.get("company_address") or doc.get("shipping_address"):
|
||||
party = frappe.db.get_value(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"parent": doc.get("company_address") or doc.get("shipping_address"),
|
||||
"parenttype": "Address",
|
||||
"link_doctype": link_doctype,
|
||||
},
|
||||
"link_name",
|
||||
)
|
||||
|
||||
if not party:
|
||||
party = parties[0].name
|
||||
else:
|
||||
party = parties[0].name
|
||||
|
||||
return party
|
||||
|
||||
|
||||
def validate_inter_company_transaction(doc, doctype):
|
||||
details = get_inter_company_details(doc, doctype)
|
||||
price_list = (
|
||||
doc.selling_price_list
|
||||
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]
|
||||
else doc.buying_price_list
|
||||
)
|
||||
valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1})
|
||||
if not valid_price_list and not doc.is_internal_transfer():
|
||||
frappe.throw(_("Selected Price List should have buying and selling fields checked."))
|
||||
|
||||
party = details.get("party")
|
||||
if not party:
|
||||
partytype = "Supplier" if doctype in ["Sales Invoice", "Sales Order"] else "Customer"
|
||||
frappe.throw(_("No {0} found for Inter Company Transactions.").format(partytype))
|
||||
|
||||
company = details.get("company")
|
||||
default_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
if default_currency != doc.currency:
|
||||
frappe.throw(
|
||||
_("Company currencies of both the companies should match for Inter Company Transactions.")
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_inter_company_purchase_invoice(source_name: str, target_doc: Document | None = None):
|
||||
return make_inter_company_transaction("Sales Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
|
||||
target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
|
||||
source_document_warehouse_field = "target_warehouse"
|
||||
target_document_warehouse_field = "from_warehouse"
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
else:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
|
||||
source_document_warehouse_field = "from_warehouse"
|
||||
target_document_warehouse_field = "target_warehouse"
|
||||
received_items = {}
|
||||
|
||||
validate_inter_company_transaction(source_doc, doctype)
|
||||
details = get_inter_company_details(source_doc, doctype)
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.run_method("set_missing_values")
|
||||
set_purchase_references(target)
|
||||
|
||||
def update_details(source_doc, target_doc, source_parent):
|
||||
def _validate_address_link(address, link_doctype, link_name):
|
||||
return frappe.db.get_value(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"parent": address,
|
||||
"parenttype": "Address",
|
||||
"link_doctype": link_doctype,
|
||||
"link_name": link_name,
|
||||
},
|
||||
"parent",
|
||||
)
|
||||
|
||||
target_doc.inter_company_invoice_reference = source_doc.name
|
||||
if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]:
|
||||
currency = frappe.db.get_value("Supplier", details.get("party"), "default_currency")
|
||||
target_doc.company = details.get("company")
|
||||
target_doc.supplier = details.get("party")
|
||||
target_doc.is_internal_supplier = 1
|
||||
target_doc.ignore_pricing_rule = 1
|
||||
target_doc.buying_price_list = source_doc.selling_price_list
|
||||
|
||||
# Invert Addresses
|
||||
if source_doc.company_address and _validate_address_link(
|
||||
source_doc.company_address, "Supplier", details.get("party")
|
||||
):
|
||||
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
|
||||
if source_doc.dispatch_address_name and _validate_address_link(
|
||||
source_doc.dispatch_address_name, "Company", details.get("company")
|
||||
):
|
||||
update_address(
|
||||
target_doc,
|
||||
"dispatch_address",
|
||||
"dispatch_address_display",
|
||||
source_doc.dispatch_address_name,
|
||||
)
|
||||
if source_doc.shipping_address_name and _validate_address_link(
|
||||
source_doc.shipping_address_name, "Company", details.get("company")
|
||||
):
|
||||
update_address(
|
||||
target_doc,
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
source_doc.shipping_address_name,
|
||||
)
|
||||
if source_doc.customer_address and _validate_address_link(
|
||||
source_doc.customer_address, "Company", details.get("company")
|
||||
):
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
)
|
||||
|
||||
if currency:
|
||||
target_doc.currency = currency
|
||||
|
||||
update_taxes(
|
||||
target_doc,
|
||||
party=target_doc.supplier,
|
||||
party_type="Supplier",
|
||||
company=target_doc.company,
|
||||
doctype=target_doc.doctype,
|
||||
party_address=target_doc.supplier_address,
|
||||
company_address=target_doc.shipping_address,
|
||||
)
|
||||
|
||||
else:
|
||||
currency = frappe.db.get_value("Customer", details.get("party"), "default_currency")
|
||||
target_doc.company = details.get("company")
|
||||
target_doc.customer = details.get("party")
|
||||
target_doc.selling_price_list = source_doc.buying_price_list
|
||||
|
||||
if source_doc.supplier_address and _validate_address_link(
|
||||
source_doc.supplier_address, "Company", details.get("company")
|
||||
):
|
||||
update_address(
|
||||
target_doc, "company_address", "company_address_display", source_doc.supplier_address
|
||||
)
|
||||
if source_doc.shipping_address and _validate_address_link(
|
||||
source_doc.shipping_address, "Customer", details.get("party")
|
||||
):
|
||||
update_address(
|
||||
target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address
|
||||
)
|
||||
if source_doc.shipping_address and _validate_address_link(
|
||||
source_doc.shipping_address, "Customer", details.get("party")
|
||||
):
|
||||
update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address)
|
||||
|
||||
if currency:
|
||||
target_doc.currency = currency
|
||||
|
||||
update_taxes(
|
||||
target_doc,
|
||||
party=target_doc.customer,
|
||||
party_type="Customer",
|
||||
company=target_doc.company,
|
||||
doctype=target_doc.doctype,
|
||||
party_address=target_doc.customer_address,
|
||||
company_address=target_doc.company_address,
|
||||
shipping_address_name=target_doc.shipping_address_name,
|
||||
)
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
target.qty = flt(source.qty) - received_items.get(source.name, 0.0)
|
||||
if source.doctype == "Purchase Order Item" and target.doctype == "Sales Order Item":
|
||||
target.purchase_order = source.parent
|
||||
target.purchase_order_item = source.name
|
||||
target.material_request = source.material_request
|
||||
target.material_request_item = source.material_request_item
|
||||
|
||||
if (
|
||||
source.get("purchase_order")
|
||||
and source.get("purchase_order_item")
|
||||
and target.doctype == "Purchase Invoice Item"
|
||||
):
|
||||
target.purchase_order = source.purchase_order
|
||||
target.po_detail = source.purchase_order_item
|
||||
|
||||
if (source.get("serial_no") or source.get("batch_no")) and not source.get("serial_and_batch_bundle"):
|
||||
target.use_serial_batch_fields = 1
|
||||
|
||||
item_field_map = {
|
||||
"doctype": target_doctype + " Item",
|
||||
"field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"],
|
||||
"field_map": {
|
||||
"rate": "rate",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.qty > 0,
|
||||
}
|
||||
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
item_field_map["field_map"].update(
|
||||
{
|
||||
"name": target_detail_field,
|
||||
}
|
||||
)
|
||||
|
||||
if source_doc.get("update_stock"):
|
||||
item_field_map["field_map"].update(
|
||||
{
|
||||
source_document_warehouse_field: target_document_warehouse_field,
|
||||
"batch_no": "batch_no",
|
||||
"serial_no": "serial_no",
|
||||
}
|
||||
)
|
||||
elif target_doctype == "Sales Order":
|
||||
item_field_map["field_map"].update(
|
||||
{
|
||||
source_document_warehouse_field: "warehouse",
|
||||
}
|
||||
)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
doctype,
|
||||
source_name,
|
||||
{
|
||||
doctype: {
|
||||
"doctype": target_doctype,
|
||||
"postprocess": update_details,
|
||||
"set_target_warehouse": "set_from_warehouse",
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"],
|
||||
},
|
||||
doctype + " Item": item_field_map,
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
reference_field = "inter_company_invoice_reference"
|
||||
if doctype == "Purchase Order":
|
||||
reference_field = "inter_company_order_reference"
|
||||
|
||||
filters = {
|
||||
reference_field: reference_name,
|
||||
"docstatus": 1,
|
||||
}
|
||||
|
||||
target_doctypes = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
if target_doctypes:
|
||||
target_doctypes = list(target_doctypes[0])
|
||||
|
||||
received_items_map = frappe._dict(
|
||||
frappe.get_all(
|
||||
doctype + " Item",
|
||||
filters={"parent": ("in", target_doctypes)},
|
||||
fields=[reference_fieldname, "qty"],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
return received_items_map
|
||||
|
||||
|
||||
def set_purchase_references(doc):
|
||||
# add internal PO or PR links if any
|
||||
|
||||
if doc.is_internal_transfer():
|
||||
if doc.doctype == "Purchase Receipt":
|
||||
so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference)
|
||||
|
||||
if so_item_map:
|
||||
pd_item_map, parent_child_map, warehouse_map = get_pd_details(
|
||||
"Purchase Order Item", so_item_map, "sales_order_item"
|
||||
)
|
||||
|
||||
update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map)
|
||||
|
||||
elif doc.doctype == "Purchase Invoice":
|
||||
dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference)
|
||||
# First check for Purchase receipt
|
||||
if list(dn_item_map.values()):
|
||||
pd_item_map, parent_child_map, warehouse_map = get_pd_details(
|
||||
"Purchase Receipt Item", dn_item_map, "delivery_note_item"
|
||||
)
|
||||
|
||||
update_pi_items(
|
||||
doc,
|
||||
"pr_detail",
|
||||
"purchase_receipt",
|
||||
dn_item_map,
|
||||
pd_item_map,
|
||||
parent_child_map,
|
||||
warehouse_map,
|
||||
)
|
||||
|
||||
|
||||
def update_pi_items(
|
||||
doc,
|
||||
detail_field,
|
||||
parent_field,
|
||||
sales_item_map,
|
||||
purchase_item_map,
|
||||
parent_child_map,
|
||||
warehouse_map,
|
||||
):
|
||||
for item in doc.get("items"):
|
||||
item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item)))
|
||||
item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item)))
|
||||
if doc.update_stock:
|
||||
item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item))
|
||||
if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"):
|
||||
item.warehouse = frappe.db.get_value(
|
||||
"Purchase Order Item", item.purchase_order_item, "warehouse"
|
||||
)
|
||||
|
||||
|
||||
def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map):
|
||||
for item in doc.get("items"):
|
||||
item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item))
|
||||
if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"):
|
||||
item.warehouse = frappe.db.get_value("Purchase Order Item", item.purchase_order_item, "warehouse")
|
||||
|
||||
|
||||
def get_delivery_note_details(internal_reference):
|
||||
si_item_details = frappe.get_all(
|
||||
"Delivery Note Item", fields=["name", "so_detail"], filters={"parent": internal_reference}
|
||||
)
|
||||
|
||||
return {d.name: d.so_detail for d in si_item_details if d.so_detail}
|
||||
|
||||
|
||||
def get_sales_invoice_details(internal_reference):
|
||||
dn_item_map = {}
|
||||
so_item_map = {}
|
||||
|
||||
si_item_details = frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
fields=["name", "so_detail", "dn_detail"],
|
||||
filters={"parent": internal_reference},
|
||||
)
|
||||
|
||||
for d in si_item_details:
|
||||
if d.dn_detail:
|
||||
dn_item_map.setdefault(d.name, d.dn_detail)
|
||||
if d.so_detail:
|
||||
so_item_map.setdefault(d.name, d.so_detail)
|
||||
|
||||
return dn_item_map, so_item_map
|
||||
|
||||
|
||||
def get_pd_details(doctype, sd_detail_map, sd_detail_field):
|
||||
pd_item_map = {}
|
||||
accepted_warehouse_map = {}
|
||||
parent_child_map = {}
|
||||
|
||||
pd_item_details = frappe.get_all(
|
||||
doctype,
|
||||
fields=[sd_detail_field, "name", "warehouse", "parent"],
|
||||
filters={sd_detail_field: ("in", list(sd_detail_map.values()))},
|
||||
)
|
||||
|
||||
for d in pd_item_details:
|
||||
pd_item_map.setdefault(d.get(sd_detail_field), d.name)
|
||||
parent_child_map.setdefault(d.get(sd_detail_field), d.parent)
|
||||
accepted_warehouse_map.setdefault(d.get(sd_detail_field), d.warehouse)
|
||||
|
||||
return pd_item_map, parent_child_map, accepted_warehouse_map
|
||||
|
||||
|
||||
def update_taxes(
|
||||
doc,
|
||||
party=None,
|
||||
party_type=None,
|
||||
company=None,
|
||||
doctype=None,
|
||||
party_address=None,
|
||||
company_address=None,
|
||||
shipping_address_name=None,
|
||||
master_doctype=None,
|
||||
):
|
||||
# Update Party Details
|
||||
party_details = _get_party_details(
|
||||
party=party,
|
||||
party_type=party_type,
|
||||
company=company,
|
||||
doctype=doctype,
|
||||
party_address=party_address,
|
||||
company_address=company_address,
|
||||
shipping_address=shipping_address_name,
|
||||
)
|
||||
|
||||
# Update taxes and charges if any
|
||||
doc.taxes_and_charges = party_details.get("taxes_and_charges")
|
||||
doc.set("taxes", party_details.get("taxes"))
|
||||
|
||||
|
||||
def update_address(doc, address_field, address_display_field, address_name):
|
||||
doc.set(address_field, address_name)
|
||||
fetch_values = get_fetch_values(doc.doctype, address_field, address_name)
|
||||
|
||||
for key, value in fetch_values.items():
|
||||
doc.set(key, value)
|
||||
|
||||
doc.set(address_display_field, get_address_display(doc.get(address_field)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name: str, target_doc: str | Document | None = None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
invoice_discounting = frappe.new_doc("Invoice Discounting")
|
||||
invoice_discounting.company = invoice.company
|
||||
invoice_discounting.append(
|
||||
"invoices",
|
||||
{
|
||||
"sales_invoice": source_name,
|
||||
"customer": invoice.customer,
|
||||
"posting_date": invoice.posting_date,
|
||||
"outstanding_amount": invoice.outstanding_amount,
|
||||
},
|
||||
)
|
||||
|
||||
return invoice_discounting
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_dunning(
|
||||
source_name: str, target_doc: str | Document | None = None, ignore_permissions: bool = False
|
||||
):
|
||||
def postprocess_dunning(source, target):
|
||||
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text
|
||||
|
||||
dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company})
|
||||
if dunning_type:
|
||||
dunning_type = frappe.get_doc("Dunning Type", dunning_type)
|
||||
target.dunning_type = dunning_type.name
|
||||
target.rate_of_interest = dunning_type.rate_of_interest
|
||||
target.dunning_fee = dunning_type.dunning_fee
|
||||
target.income_account = dunning_type.income_account
|
||||
target.cost_center = dunning_type.cost_center
|
||||
letter_text = get_dunning_letter_text(
|
||||
dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
|
||||
)
|
||||
|
||||
if letter_text:
|
||||
target.body_text = letter_text.get("body_text")
|
||||
target.closing_text = letter_text.get("closing_text")
|
||||
target.language = letter_text.get("language")
|
||||
|
||||
# update outstanding from doc
|
||||
if source.payment_schedule and len(source.payment_schedule) == 1:
|
||||
for row in target.overdue_payments:
|
||||
if row.payment_schedule == source.payment_schedule[0].name:
|
||||
row.outstanding = source.get("outstanding_amount")
|
||||
|
||||
target.validate()
|
||||
|
||||
return get_mapped_doc(
|
||||
from_doctype="Sales Invoice",
|
||||
from_docname=source_name,
|
||||
target_doc=target_doc,
|
||||
table_maps={
|
||||
"Sales Invoice": {
|
||||
"doctype": "Dunning",
|
||||
"field_map": {"customer_address": "customer_address", "parent": "sales_invoice"},
|
||||
},
|
||||
"Payment Schedule": {
|
||||
"doctype": "Overdue Payment",
|
||||
"field_map": {"name": "payment_schedule", "parent": "sales_invoice"},
|
||||
"condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(),
|
||||
},
|
||||
},
|
||||
postprocess=postprocess_dunning,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
@@ -197,21 +197,21 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
make_invoice_discounting() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
method: "erpnext.accounts.doctype.sales_invoice.mapper.create_invoice_discounting",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_dunning() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
method: "erpnext.accounts.doctype.sales_invoice.mapper.create_dunning",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_maintenance_schedule() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
||||
method: "erpnext.accounts.doctype.sales_invoice.mapper.make_maintenance_schedule",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
@@ -361,7 +361,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
__("Sales Order"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice",
|
||||
method: "erpnext.selling.doctype.sales_order.mapper.make_sales_invoice",
|
||||
source_doctype: "Sales Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
@@ -383,7 +383,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
__("Quotation"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.quotation.quotation.make_sales_invoice",
|
||||
method: "erpnext.selling.doctype.quotation.mapper.make_sales_invoice",
|
||||
source_doctype: "Quotation",
|
||||
target: me.frm,
|
||||
setters: [
|
||||
@@ -421,7 +421,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
method: "erpnext.stock.doctype.delivery_note.mapper.make_sales_invoice",
|
||||
source_doctype: "Delivery Note",
|
||||
target: me.frm,
|
||||
date_field: "posting_date",
|
||||
@@ -501,7 +501,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
make_inter_company_invoice() {
|
||||
let me = this;
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
|
||||
method: "erpnext.accounts.doctype.sales_invoice.mapper.make_inter_company_purchase_invoice",
|
||||
frm: me.frm,
|
||||
});
|
||||
}
|
||||
@@ -579,7 +579,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
make_sales_return() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_sales_return",
|
||||
method: "erpnext.accounts.doctype.sales_invoice.mapper.make_sales_return",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
@@ -712,7 +712,7 @@ extend_cscript(cur_frm.cscript, new erpnext.accounts.SalesInvoiceController({ fr
|
||||
|
||||
cur_frm.cscript["Make Delivery Note"] = function () {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_delivery_note",
|
||||
method: "erpnext.accounts.doctype.sales_invoice.mapper.make_delivery_note",
|
||||
frm: cur_frm,
|
||||
});
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
173
erpnext/accounts/doctype/sales_invoice/services/fixed_assets.py
Normal file
173
erpnext/accounts/doctype/sales_invoice/services/fixed_assets.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Fixed asset lifecycle helpers for Sales Invoice."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
reset_depreciation_schedule,
|
||||
reverse_depreciation_entry_made_on_disposal,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.mapper import split_asset
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
|
||||
|
||||
class FixedAssetService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def validate_fixed_asset(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.doctype != "Sales Invoice":
|
||||
return
|
||||
|
||||
for d in doc.get("items"):
|
||||
if not d.is_fixed_asset:
|
||||
continue
|
||||
|
||||
if d.asset:
|
||||
if not doc.is_return:
|
||||
asset_status = frappe.db.get_value("Asset", d.asset, "status")
|
||||
if doc.update_stock:
|
||||
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
|
||||
elif asset_status in ("Scrapped", "Cancelled", "Capitalized"):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Asset {1} cannot be sold, it is already {2}").format(
|
||||
d.idx, d.asset, asset_status
|
||||
)
|
||||
)
|
||||
elif asset_status == "Sold" and not doc.is_return:
|
||||
frappe.throw(_("Row #{0}: Asset {1} is already sold").format(d.idx, d.asset))
|
||||
elif not doc.return_against:
|
||||
frappe.throw(_("Row #{0}: Return Against is required for returning asset").format(d.idx))
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Row #{0}: You must select an Asset for Item {1}.").format(d.idx, d.item_code),
|
||||
title=_("Missing Asset"),
|
||||
)
|
||||
|
||||
def set_income_account_for_fixed_assets(self) -> None:
|
||||
for item in self.doc.items:
|
||||
item.set_income_account_for_fixed_asset(self.doc.company)
|
||||
|
||||
def process_asset_depreciation(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.is_internal_transfer():
|
||||
return
|
||||
|
||||
if (doc.is_return and doc.docstatus == 2) or (not doc.is_return and doc.docstatus == 1):
|
||||
self._depreciate_asset_on_sale()
|
||||
else:
|
||||
self._restore_asset()
|
||||
|
||||
self._update_asset()
|
||||
|
||||
def split_asset_based_on_sale_qty(self) -> None:
|
||||
asset_qty_map = self._get_asset_qty()
|
||||
for asset, qty in asset_qty_map.items():
|
||||
if qty["actual_qty"] < qty["sale_qty"]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)."
|
||||
).format(asset, qty["actual_qty"])
|
||||
)
|
||||
|
||||
remaining_qty = qty["actual_qty"] - qty["sale_qty"]
|
||||
if remaining_qty > 0:
|
||||
split_asset(asset, remaining_qty)
|
||||
|
||||
def get_disposal_date(self) -> str:
|
||||
doc = self.doc
|
||||
if doc.is_return:
|
||||
return frappe.db.get_value("Sales Invoice", doc.return_against, "posting_date")
|
||||
return doc.posting_date
|
||||
|
||||
def _depreciate_asset_on_sale(self) -> None:
|
||||
disposal_date = self.get_disposal_date()
|
||||
for d in self.doc.get("items"):
|
||||
if d.asset:
|
||||
asset = frappe.get_doc("Asset", d.asset)
|
||||
if asset.calculate_depreciation and asset.status != "Fully Depreciated":
|
||||
depreciate_asset(asset, disposal_date, self._get_note_for_asset_sale(asset))
|
||||
|
||||
def _restore_asset(self) -> None:
|
||||
for d in self.doc.get("items"):
|
||||
if d.asset:
|
||||
asset = frappe.get_cached_doc("Asset", d.asset)
|
||||
if asset.calculate_depreciation:
|
||||
reverse_depreciation_entry_made_on_disposal(asset)
|
||||
reset_depreciation_schedule(asset, self._get_note_for_asset_return(asset))
|
||||
|
||||
def _update_asset(self) -> None:
|
||||
doc = self.doc
|
||||
disposal_date = self.get_disposal_date()
|
||||
|
||||
for d in doc.get("items"):
|
||||
if not d.asset:
|
||||
continue
|
||||
|
||||
asset = frappe.get_cached_doc("Asset", d.asset)
|
||||
|
||||
if (doc.is_return and doc.docstatus == 1) or (not doc.is_return and doc.docstatus == 2):
|
||||
note = _("Asset returned") if doc.is_return else _("Asset sold")
|
||||
asset_status, disposal_date = None, None
|
||||
else:
|
||||
note = _("Asset sold") if not doc.is_return else _("Return invoice of asset cancelled")
|
||||
asset_status = "Sold"
|
||||
|
||||
frappe.db.set_value("Asset", d.asset, "disposal_date", disposal_date)
|
||||
add_asset_activity(asset.name, note)
|
||||
asset.set_status(asset_status)
|
||||
|
||||
def _get_asset_qty(self) -> dict:
|
||||
doc = self.doc
|
||||
asset_qty_map = {}
|
||||
|
||||
assets = {row.asset for row in doc.items if row.is_fixed_asset and row.asset}
|
||||
if not assets or doc.is_return:
|
||||
return asset_qty_map
|
||||
|
||||
asset_actual_qty = dict(
|
||||
frappe.db.get_all(
|
||||
"Asset",
|
||||
{"name": ["in", list(assets)]},
|
||||
["name", "asset_quantity"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
for row in doc.items:
|
||||
if row.is_fixed_asset and row.asset:
|
||||
actual_qty = asset_actual_qty.get(row.asset)
|
||||
if row.asset in asset_qty_map:
|
||||
asset_qty_map[row.asset]["sale_qty"] += flt(row.qty)
|
||||
else:
|
||||
asset_qty_map[row.asset] = {
|
||||
"sale_qty": flt(row.qty),
|
||||
"actual_qty": flt(actual_qty),
|
||||
}
|
||||
|
||||
return asset_qty_map
|
||||
|
||||
def _get_note_for_asset_sale(self, asset) -> str:
|
||||
doc = self.doc
|
||||
return _("This schedule was created when Asset {0} was {1} through Sales Invoice {2}.").format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
_("returned") if doc.is_return else _("sold"),
|
||||
get_link_to_form(doc.doctype, doc.get("name")),
|
||||
)
|
||||
|
||||
def _get_note_for_asset_return(self, asset) -> str:
|
||||
doc = self.doc
|
||||
asset_link = get_link_to_form(asset.doctype, asset.name)
|
||||
invoice_link = get_link_to_form(doc.doctype, doc.get("name"))
|
||||
if doc.is_return:
|
||||
return _(
|
||||
"This schedule was created when Asset {0} was returned through Sales Invoice {1}."
|
||||
).format(asset_link, invoice_link)
|
||||
return _(
|
||||
"This schedule was created when Asset {0} was restored due to Sales Invoice {1} cancellation."
|
||||
).format(asset_link, invoice_link)
|
||||
661
erpnext/accounts/doctype/sales_invoice/services/gl_composer.py
Normal file
661
erpnext/accounts/doctype/sales_invoice/services/gl_composer.py
Normal file
@@ -0,0 +1,661 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.services.base_gl_composer import BaseGLComposer
|
||||
from erpnext.accounts.services.taxes import TaxService
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
)
|
||||
|
||||
|
||||
class SalesInvoiceGLComposer(BaseGLComposer):
|
||||
"""Assembles the GL entries for a Sales Invoice."""
|
||||
|
||||
def compose(self, inventory_account_map=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_regional_gl_entries
|
||||
from erpnext.accounts.general_ledger import merge_similar_entries
|
||||
|
||||
doc = self.doc
|
||||
gl_entries = []
|
||||
|
||||
self.make_customer_gl_entry(gl_entries)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
|
||||
disable_sdbnb_in_sr = frappe.get_cached_value("Company", doc.company, "disable_sdbnb_in_sr")
|
||||
|
||||
if not (doc.is_return and disable_sdbnb_in_sr):
|
||||
self.stock_delivered_but_not_billed_gl_entries(gl_entries)
|
||||
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, doc)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
gl_entries = merge_similar_entries(gl_entries)
|
||||
|
||||
self.make_loyalty_point_redemption_gle(gl_entries)
|
||||
self.make_pos_gl_entries(gl_entries)
|
||||
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
|
||||
doc.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
doc = self.doc
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
_round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
doc.company, "Sales Invoice", doc.name, doc.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
precision_loss = doc.get("base_net_total") - flt(
|
||||
doc.get("net_total") * doc.conversion_rate, doc.precision("net_total")
|
||||
)
|
||||
|
||||
if precision_loss:
|
||||
gl_entries.append(
|
||||
doc.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": doc.customer,
|
||||
"debit": precision_loss,
|
||||
"cost_center": round_off_cost_center
|
||||
if doc.use_company_roundoff_cost_center
|
||||
else doc.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def make_discount_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
enable_discount_accounting = cint(
|
||||
frappe.get_single_value("Selling Settings", "enable_discount_accounting")
|
||||
)
|
||||
|
||||
if enable_discount_accounting:
|
||||
for item in doc.get("items"):
|
||||
if item.get("discount_amount") and item.get("discount_account"):
|
||||
discount_amount = item.discount_amount * item.qty
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or doc.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
|
||||
account_currency = get_account_currency(item.discount_account)
|
||||
gl_entries.append(
|
||||
doc.get_gl_dict(
|
||||
{
|
||||
"account": item.discount_account,
|
||||
"against": doc.customer,
|
||||
"debit": flt(
|
||||
discount_amount * doc.get("conversion_rate"),
|
||||
item.precision("discount_amount"),
|
||||
),
|
||||
"debit_in_transaction_currency": flt(
|
||||
discount_amount, item.precision("discount_amount")
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
doc.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": doc.customer,
|
||||
"credit": flt(
|
||||
discount_amount * doc.get("conversion_rate"),
|
||||
item.precision("discount_amount"),
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
discount_amount, item.precision("discount_amount")
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
(enable_discount_accounting or doc.get("is_cash_or_non_trade_discount"))
|
||||
and doc.get("additional_discount_account")
|
||||
and doc.get("discount_amount")
|
||||
):
|
||||
gl_entries.append(
|
||||
doc.get_gl_dict(
|
||||
{
|
||||
"account": doc.additional_discount_account,
|
||||
"against": doc.customer,
|
||||
"debit": doc.base_discount_amount,
|
||||
"cost_center": doc.cost_center or erpnext.get_default_cost_center(doc.company),
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def stock_delivered_but_not_billed_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
if doc.update_stock or not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
|
||||
return
|
||||
|
||||
for item in doc.get("items"):
|
||||
if not item.delivery_note and not item.dn_detail:
|
||||
continue
|
||||
|
||||
if not frappe.get_cached_value("Item", item.item_code, "is_stock_item"):
|
||||
continue
|
||||
|
||||
dn_expense_account = frappe.get_cached_value(
|
||||
"Delivery Note Item", item.dn_detail, "expense_account"
|
||||
)
|
||||
if (
|
||||
not dn_expense_account
|
||||
or frappe.get_cached_value("Account", dn_expense_account, "account_type")
|
||||
!= "Stock Delivered But Not Billed"
|
||||
or not item.expense_account
|
||||
or dn_expense_account == item.expense_account
|
||||
):
|
||||
continue
|
||||
|
||||
delivery_note = item.delivery_note or frappe.get_cached_value(
|
||||
"Delivery Note Item", item.dn_detail, "parent"
|
||||
)
|
||||
if not delivery_note:
|
||||
continue
|
||||
|
||||
item_g = frappe.get_cached_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_no": delivery_note,
|
||||
"voucher_detail_no": item.dn_detail,
|
||||
"item_code": item.item_code,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
["stock_value_difference", "actual_qty"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not item_g or not flt(item_g.actual_qty):
|
||||
continue
|
||||
valuation_rate = flt(item_g.stock_value_difference) / flt(item_g.actual_qty)
|
||||
valuation_amount = valuation_rate * item.stock_qty
|
||||
dn_account_currency = get_account_currency(dn_expense_account)
|
||||
item_account_currency = get_account_currency(item.expense_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": dn_expense_account,
|
||||
"against": item.expense_account,
|
||||
"credit": flt(valuation_amount),
|
||||
"credit_in_account_currency": flt(valuation_amount),
|
||||
"cost_center": item.cost_center,
|
||||
},
|
||||
dn_account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account,
|
||||
"against": dn_expense_account,
|
||||
"debit": flt(valuation_amount),
|
||||
"debit_in_account_currency": flt(valuation_amount),
|
||||
"cost_center": item.cost_center,
|
||||
},
|
||||
item_account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
def make_customer_gl_entry(self, gl_entries):
|
||||
doc = self.doc
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introduction of posting GLE based on rounded total
|
||||
grand_total = (
|
||||
doc.rounded_total if (doc.rounding_adjustment and doc.rounded_total) else doc.grand_total
|
||||
)
|
||||
base_grand_total = flt(
|
||||
doc.base_rounded_total
|
||||
if (doc.base_rounding_adjustment and doc.base_rounded_total)
|
||||
else doc.base_grand_total,
|
||||
doc.precision("base_grand_total"),
|
||||
)
|
||||
|
||||
if grand_total and not doc.is_internal_transfer():
|
||||
against_voucher = doc.name
|
||||
if doc.is_return and doc.return_against and not doc.update_outstanding_for_self:
|
||||
against_voucher = doc.return_against
|
||||
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": doc.customer,
|
||||
"due_date": doc.due_date,
|
||||
"against": doc.against_income_account,
|
||||
"debit": base_grand_total,
|
||||
"debit_in_account_currency": base_grand_total
|
||||
if doc.party_account_currency == doc.company_currency
|
||||
else grand_total,
|
||||
"debit_in_transaction_currency": grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": doc.doctype,
|
||||
"cost_center": doc.cost_center,
|
||||
"project": doc.project,
|
||||
},
|
||||
doc.party_account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
tax_service = TaxService(doc)
|
||||
enable_discount_accounting = cint(
|
||||
frappe.get_single_value("Selling Settings", "enable_discount_accounting")
|
||||
)
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
amount, base_amount = tax_service.get_tax_amounts(tax, enable_discount_accounting)
|
||||
|
||||
if flt(tax.base_tax_amount_after_discount_amount):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": tax.account_head,
|
||||
"against": doc.customer,
|
||||
"credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, tax.precision("base_tax_amount_after_discount_amount"))
|
||||
if account_currency == doc.company_currency
|
||||
else flt(amount, tax.precision("tax_amount_after_discount_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
amount, tax.precision("tax_amount_after_discount_amount")
|
||||
),
|
||||
"cost_center": tax.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
item=tax,
|
||||
)
|
||||
)
|
||||
|
||||
def make_internal_transfer_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
if doc.is_internal_transfer() and flt(doc.base_total_taxes_and_charges):
|
||||
account_currency = get_account_currency(doc.unrealized_profit_loss_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.unrealized_profit_loss_account,
|
||||
"against": doc.customer,
|
||||
"debit": flt(doc.total_taxes_and_charges),
|
||||
"debit_in_account_currency": flt(doc.base_total_taxes_and_charges),
|
||||
"debit_in_transaction_currency": flt(doc.total_taxes_and_charges),
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def make_item_gl_entries(self, gl_entries):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
|
||||
|
||||
doc = self.doc
|
||||
tax_service = TaxService(doc)
|
||||
# income account gl entries
|
||||
enable_discount_accounting = cint(
|
||||
frappe.get_single_value("Selling Settings", "enable_discount_accounting")
|
||||
)
|
||||
|
||||
for item in doc.get("items"):
|
||||
if (
|
||||
flt(item.base_net_amount, item.precision("base_net_amount"))
|
||||
or item.is_fixed_asset
|
||||
or enable_discount_accounting
|
||||
):
|
||||
# Do not book income for transfer within same company
|
||||
if doc.is_internal_transfer():
|
||||
continue
|
||||
|
||||
if item.is_fixed_asset and item.asset:
|
||||
self.get_gl_entries_for_fixed_asset(item, gl_entries)
|
||||
else:
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or doc.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
|
||||
amount, base_amount = tax_service.get_amount_and_base_amount(
|
||||
item, enable_discount_accounting
|
||||
)
|
||||
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": doc.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == doc.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(amount, item.precision("net_amount")),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# expense account gl entries
|
||||
if cint(doc.update_stock) and erpnext.is_perpetual_inventory_enabled(doc.company):
|
||||
gl_entries += super(SalesInvoice, doc).get_gl_entries()
|
||||
|
||||
def get_gl_entries_for_fixed_asset(self, item, gl_entries):
|
||||
doc = self.doc
|
||||
asset = frappe.get_cached_doc("Asset", item.asset)
|
||||
|
||||
if doc.is_return:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
item.finance_book,
|
||||
doc.get("doctype"),
|
||||
doc.get("name"),
|
||||
doc.get("posting_date"),
|
||||
)
|
||||
else:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
item.finance_book,
|
||||
doc.get("doctype"),
|
||||
doc.get("name"),
|
||||
doc.get("posting_date"),
|
||||
)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = doc.customer
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
|
||||
def make_loyalty_point_redemption_gle(self, gl_entries):
|
||||
doc = self.doc
|
||||
if cint(doc.redeem_loyalty_points and doc.loyalty_points and not doc.is_consolidated):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": doc.customer,
|
||||
"against": "Expense account - "
|
||||
+ cstr(doc.loyalty_redemption_account)
|
||||
+ " for the Loyalty Program",
|
||||
"credit": doc.loyalty_amount,
|
||||
"credit_in_transaction_currency": doc.loyalty_amount,
|
||||
"against_voucher": doc.return_against if cint(doc.is_return) else doc.name,
|
||||
"against_voucher_type": doc.doctype,
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.loyalty_redemption_account,
|
||||
"cost_center": doc.cost_center or doc.loyalty_redemption_cost_center,
|
||||
"against": doc.customer,
|
||||
"debit": doc.loyalty_amount,
|
||||
"debit_in_transaction_currency": doc.loyalty_amount,
|
||||
"remark": "Loyalty Points redeemed by the customer",
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def make_pos_gl_entries(self, gl_entries):
|
||||
doc = self.doc
|
||||
if cint(doc.is_pos):
|
||||
skip_change_gl_entries = not cint(
|
||||
frappe.get_single_value("POS Settings", "post_change_gl_entries")
|
||||
)
|
||||
|
||||
for payment_mode in doc.payments:
|
||||
if skip_change_gl_entries and payment_mode.account == doc.account_for_change_amount:
|
||||
payment_mode.base_amount -= flt(doc.change_amount)
|
||||
|
||||
against_voucher = doc.name
|
||||
if doc.is_return and doc.return_against and not doc.update_outstanding_for_self:
|
||||
against_voucher = doc.return_against
|
||||
|
||||
if payment_mode.base_amount:
|
||||
# POS, make payment entries
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": doc.customer,
|
||||
"against": payment_mode.account,
|
||||
"credit": payment_mode.base_amount,
|
||||
"credit_in_account_currency": payment_mode.base_amount
|
||||
if doc.party_account_currency == doc.company_currency
|
||||
else payment_mode.amount,
|
||||
"credit_in_transaction_currency": payment_mode.amount,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": doc.doctype,
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
doc.party_account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
payment_mode_account_currency = get_account_currency(payment_mode.account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": payment_mode.account,
|
||||
"against": doc.customer,
|
||||
"debit": payment_mode.base_amount,
|
||||
"debit_in_account_currency": payment_mode.base_amount
|
||||
if payment_mode_account_currency == doc.company_currency
|
||||
else payment_mode.amount,
|
||||
"debit_in_transaction_currency": payment_mode.amount,
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
payment_mode_account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
if not skip_change_gl_entries:
|
||||
gl_entries.extend(self.get_gle_for_change_amount())
|
||||
|
||||
def get_gle_for_change_amount(self) -> list[dict]:
|
||||
doc = self.doc
|
||||
if not doc.change_amount:
|
||||
return []
|
||||
|
||||
if not doc.account_for_change_amount:
|
||||
frappe.throw(_("Please set Account for Change Amount"), title=_("Mandatory Field"))
|
||||
|
||||
return [
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": doc.customer,
|
||||
"against": doc.account_for_change_amount,
|
||||
"debit": flt(doc.base_change_amount),
|
||||
"debit_in_account_currency": flt(doc.base_change_amount)
|
||||
if doc.party_account_currency == doc.company_currency
|
||||
else flt(doc.change_amount),
|
||||
"debit_in_transaction_currency": flt(doc.change_amount),
|
||||
"against_voucher": doc.return_against
|
||||
if cint(doc.is_return) and doc.return_against
|
||||
else doc.name,
|
||||
"against_voucher_type": doc.doctype,
|
||||
"cost_center": doc.cost_center,
|
||||
"project": doc.project,
|
||||
},
|
||||
doc.party_account_currency,
|
||||
item=doc,
|
||||
),
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.account_for_change_amount,
|
||||
"against": doc.customer,
|
||||
"credit": doc.base_change_amount,
|
||||
"credit_in_transaction_currency": doc.change_amount,
|
||||
"cost_center": doc.cost_center,
|
||||
},
|
||||
item=doc,
|
||||
),
|
||||
]
|
||||
|
||||
def make_write_off_gl_entry(self, gl_entries):
|
||||
doc = self.doc
|
||||
# write off entries, applicable if only pos
|
||||
if (
|
||||
doc.is_pos
|
||||
and doc.write_off_account
|
||||
and flt(doc.write_off_amount, doc.precision("write_off_amount"))
|
||||
):
|
||||
write_off_account_currency = get_account_currency(doc.write_off_account)
|
||||
default_cost_center = frappe.get_cached_value("Company", doc.company, "cost_center")
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": doc.customer,
|
||||
"against": doc.write_off_account,
|
||||
"credit": flt(doc.base_write_off_amount, doc.precision("base_write_off_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(doc.base_write_off_amount, doc.precision("base_write_off_amount"))
|
||||
if doc.party_account_currency == doc.company_currency
|
||||
else flt(doc.write_off_amount, doc.precision("write_off_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
doc.write_off_amount, doc.precision("write_off_amount")
|
||||
),
|
||||
"against_voucher": doc.return_against if cint(doc.is_return) else doc.name,
|
||||
"against_voucher_type": doc.doctype,
|
||||
"cost_center": doc.cost_center,
|
||||
"project": doc.project,
|
||||
},
|
||||
doc.party_account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": doc.write_off_account,
|
||||
"against": doc.customer,
|
||||
"debit": flt(doc.base_write_off_amount, doc.precision("base_write_off_amount")),
|
||||
"debit_in_account_currency": (
|
||||
flt(doc.base_write_off_amount, doc.precision("base_write_off_amount"))
|
||||
if write_off_account_currency == doc.company_currency
|
||||
else flt(doc.write_off_amount, doc.precision("write_off_amount"))
|
||||
),
|
||||
"debit_in_transaction_currency": flt(
|
||||
doc.write_off_amount, doc.precision("write_off_amount")
|
||||
),
|
||||
"cost_center": doc.cost_center or doc.write_off_cost_center or default_cost_center,
|
||||
},
|
||||
write_off_account_currency,
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def make_gle_for_rounding_adjustment(self, gl_entries):
|
||||
doc = self.doc
|
||||
if (
|
||||
flt(doc.rounding_adjustment, doc.precision("rounding_adjustment"))
|
||||
and doc.base_rounding_adjustment
|
||||
and not doc.is_internal_transfer()
|
||||
):
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
doc.company, "Sales Invoice", doc.name, doc.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
if doc.is_opening == "Yes" and doc.rounding_adjustment:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||
).format(
|
||||
frappe.bold(doc.rounding_adjustment),
|
||||
frappe.bold("Round Off for Opening"),
|
||||
get_link_to_form("Company", doc.company),
|
||||
frappe.bold("Disable Rounded Total"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
round_off_account = round_off_for_opening
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": doc.customer,
|
||||
"credit_in_account_currency": flt(
|
||||
doc.rounding_adjustment, doc.precision("rounding_adjustment")
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
doc.rounding_adjustment, doc.precision("rounding_adjustment")
|
||||
),
|
||||
"credit": flt(
|
||||
doc.base_rounding_adjustment, doc.precision("base_rounding_adjustment")
|
||||
),
|
||||
"cost_center": round_off_cost_center
|
||||
if doc.use_company_roundoff_cost_center
|
||||
else (doc.cost_center or round_off_cost_center),
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Inter-company transaction helpers for Sales Invoice."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
|
||||
def validate_inter_company_party(
|
||||
doctype: str, party: str, company: str, inter_company_reference: str | None
|
||||
) -> None:
|
||||
if not party:
|
||||
return
|
||||
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
partytype, ref_partytype, internal = "Customer", "Supplier", "is_internal_customer"
|
||||
ref_doc = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
|
||||
else:
|
||||
partytype, ref_partytype, internal = "Supplier", "Customer", "is_internal_supplier"
|
||||
ref_doc = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
|
||||
|
||||
if inter_company_reference:
|
||||
doc = frappe.get_doc(ref_doc, inter_company_reference)
|
||||
ref_party = doc.supplier if doctype in ["Sales Invoice", "Sales Order"] else doc.customer
|
||||
if frappe.db.get_value(partytype, {"represents_company": doc.company}, "name") != party:
|
||||
frappe.throw(_("Invalid {0} for Inter Company Transaction.").format(_(partytype)))
|
||||
if frappe.get_cached_value(ref_partytype, ref_party, "represents_company") != company:
|
||||
frappe.throw(_("Invalid Company for Inter Company Transaction."))
|
||||
|
||||
elif frappe.db.get_value(partytype, {"name": party, internal: 1}, "name") == party:
|
||||
companies = [
|
||||
d.company
|
||||
for d in frappe.get_all(
|
||||
"Allowed To Transact With",
|
||||
fields=["company"],
|
||||
filters={"parenttype": partytype, "parent": party},
|
||||
)
|
||||
]
|
||||
if company not in companies:
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} not allowed to transact with {1}. Please change the Company or add the Company in the 'Allowed To Transact With'-Section in the Customer record."
|
||||
).format(_(partytype), company)
|
||||
)
|
||||
|
||||
|
||||
def update_linked_doc(doctype: str, name: str, inter_company_reference: str | None) -> None:
|
||||
ref_field = (
|
||||
"inter_company_invoice_reference"
|
||||
if doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
else "inter_company_order_reference"
|
||||
)
|
||||
if inter_company_reference:
|
||||
frappe.db.set_value(doctype, inter_company_reference, ref_field, name)
|
||||
|
||||
|
||||
def unlink_inter_company_doc(doctype: str, name: str, inter_company_reference: str | None) -> None:
|
||||
if doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
ref_doc = "Purchase Invoice" if doctype == "Sales Invoice" else "Sales Invoice"
|
||||
ref_field = "inter_company_invoice_reference"
|
||||
else:
|
||||
ref_doc = "Purchase Order" if doctype == "Sales Order" else "Sales Order"
|
||||
ref_field = "inter_company_order_reference"
|
||||
|
||||
if inter_company_reference:
|
||||
frappe.db.set_value(doctype, name, ref_field, "")
|
||||
frappe.db.set_value(ref_doc, inter_company_reference, ref_field, "")
|
||||
163
erpnext/accounts/doctype/sales_invoice/services/loyalty.py
Normal file
163
erpnext/accounts/doctype/sales_invoice/services/loyalty.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Loyalty program helpers for Sales Invoice."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, cint, flt, getdate
|
||||
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
get_loyalty_program_details_with_points,
|
||||
)
|
||||
|
||||
|
||||
class LoyaltyService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def make_loyalty_point_entry(self) -> None:
|
||||
doc = self.doc
|
||||
returned_amount = self._get_returned_amount()
|
||||
current_amount = flt(doc.grand_total) - cint(doc.loyalty_amount)
|
||||
eligible_amount = current_amount - returned_amount
|
||||
lp_details = get_loyalty_program_details_with_points(
|
||||
doc.customer,
|
||||
company=doc.company,
|
||||
current_transaction_amount=current_amount,
|
||||
loyalty_program=doc.loyalty_program,
|
||||
expiry_date=doc.posting_date,
|
||||
include_expired_entry=True,
|
||||
)
|
||||
if (
|
||||
lp_details
|
||||
and getdate(lp_details.from_date) <= getdate(doc.posting_date)
|
||||
and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(doc.posting_date))
|
||||
):
|
||||
collection_factor = lp_details.collection_factor if lp_details.collection_factor else 1.0
|
||||
points_earned = cint(eligible_amount / collection_factor)
|
||||
|
||||
entry = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Loyalty Point Entry",
|
||||
"company": doc.company,
|
||||
"loyalty_program": lp_details.loyalty_program,
|
||||
"loyalty_program_tier": lp_details.tier_name,
|
||||
"customer": doc.customer,
|
||||
"invoice_type": doc.doctype,
|
||||
"invoice": doc.name,
|
||||
"loyalty_points": points_earned,
|
||||
"purchase_amount": eligible_amount,
|
||||
"expiry_date": add_days(doc.posting_date, lp_details.expiry_duration),
|
||||
"posting_date": doc.posting_date,
|
||||
}
|
||||
)
|
||||
entry.flags.ignore_permissions = 1
|
||||
entry.save()
|
||||
self._set_loyalty_program_tier()
|
||||
|
||||
def delete_loyalty_point_entry(self) -> None:
|
||||
doc = self.doc
|
||||
lp_entry = frappe.db.get_all(
|
||||
"Loyalty Point Entry", filters={"invoice": doc.name, "loyalty_points": (">", 0)}, fields=["name"]
|
||||
)
|
||||
|
||||
if not lp_entry:
|
||||
return
|
||||
|
||||
against_lp_entry = frappe.db.get_all(
|
||||
"Loyalty Point Entry",
|
||||
filters={"redeem_against": lp_entry[0].name},
|
||||
fields=["name", "invoice"],
|
||||
)
|
||||
|
||||
if against_lp_entry:
|
||||
invoice_list = ", ".join([d.invoice for d in against_lp_entry])
|
||||
frappe.throw(
|
||||
_(
|
||||
"{} can't be cancelled since the Loyalty Points earned has been redeemed. "
|
||||
"First cancel the {} No {}"
|
||||
).format(doc.doctype, doc.doctype, invoice_list)
|
||||
)
|
||||
else:
|
||||
frappe.db.delete("Loyalty Point Entry", filters={"invoice": doc.name})
|
||||
self._set_loyalty_program_tier()
|
||||
|
||||
def apply_loyalty_points(self) -> None:
|
||||
from erpnext.accounts.doctype.loyalty_point_entry.loyalty_point_entry import (
|
||||
get_loyalty_point_entries,
|
||||
get_redemption_details,
|
||||
)
|
||||
|
||||
doc = self.doc
|
||||
loyalty_point_entries = get_loyalty_point_entries(
|
||||
doc.customer, doc.loyalty_program, doc.company, doc.posting_date
|
||||
)
|
||||
redemption_details = get_redemption_details(doc.customer, doc.loyalty_program, doc.company)
|
||||
|
||||
points_to_redeem = doc.loyalty_points
|
||||
for lp_entry in loyalty_point_entries:
|
||||
if lp_entry.invoice_type != doc.doctype or lp_entry.invoice == doc.name:
|
||||
continue
|
||||
available_points = lp_entry.loyalty_points - flt(redemption_details.get(lp_entry.name))
|
||||
redeemed_points = min(available_points, points_to_redeem)
|
||||
entry = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Loyalty Point Entry",
|
||||
"company": doc.company,
|
||||
"loyalty_program": doc.loyalty_program,
|
||||
"loyalty_program_tier": lp_entry.loyalty_program_tier,
|
||||
"customer": doc.customer,
|
||||
"invoice_type": doc.doctype,
|
||||
"invoice": doc.name,
|
||||
"redeem_against": lp_entry.name,
|
||||
"loyalty_points": -1 * redeemed_points,
|
||||
"purchase_amount": doc.grand_total,
|
||||
"expiry_date": lp_entry.expiry_date,
|
||||
"posting_date": doc.posting_date,
|
||||
}
|
||||
)
|
||||
entry.flags.ignore_permissions = 1
|
||||
entry.save()
|
||||
points_to_redeem -= redeemed_points
|
||||
if points_to_redeem < 1:
|
||||
break
|
||||
|
||||
def _set_loyalty_program_tier(self) -> None:
|
||||
doc = self.doc
|
||||
lp_details = get_loyalty_program_details_with_points(
|
||||
doc.customer,
|
||||
company=doc.company,
|
||||
loyalty_program=doc.loyalty_program,
|
||||
include_expired_entry=True,
|
||||
)
|
||||
customer = frappe.get_doc("Customer", doc.customer)
|
||||
customer.db_set("loyalty_program_tier", lp_details.tier_name)
|
||||
|
||||
def _get_returned_amount(self) -> float:
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
doc = frappe.qb.DocType(self.doc.doctype)
|
||||
returned_amount = (
|
||||
frappe.qb.from_(doc)
|
||||
.select(Sum(doc.grand_total))
|
||||
.where((doc.docstatus == 1) & (doc.is_return == 1) & (doc.return_against == self.doc.name))
|
||||
).run()
|
||||
|
||||
return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
|
||||
|
||||
|
||||
def get_loyalty_programs(customer: str) -> list:
|
||||
"""Return applicable loyalty programs for the customer."""
|
||||
from erpnext.selling.doctype.customer.customer import get_loyalty_programs as _get
|
||||
|
||||
customer_doc = frappe.get_doc("Customer", customer)
|
||||
if customer_doc.loyalty_program:
|
||||
return [customer_doc.loyalty_program]
|
||||
|
||||
lp_details = _get(customer_doc)
|
||||
|
||||
if len(lp_details) == 1:
|
||||
customer_doc.db_set("loyalty_program", lp_details[0])
|
||||
|
||||
return lp_details
|
||||
422
erpnext/accounts/doctype/sales_invoice/services/pos.py
Normal file
422
erpnext/accounts/doctype/sales_invoice/services/pos.py
Normal file
@@ -0,0 +1,422 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""POS helpers for Sales Invoice."""
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
|
||||
class PartialPaymentValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class POSService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def set_pos_fields(self, for_validate: bool = False) -> frappe.Document | None:
|
||||
"""Populate POS-profile fields on the invoice; return the profile or None."""
|
||||
doc = self.doc
|
||||
if cint(doc.is_pos) != 1:
|
||||
return None
|
||||
|
||||
if not doc.account_for_change_amount:
|
||||
doc.account_for_change_amount = frappe.get_cached_value(
|
||||
"Company", doc.company, "default_cash_account"
|
||||
)
|
||||
|
||||
from erpnext.stock.get_item_details import (
|
||||
ItemDetailsCtx,
|
||||
get_pos_profile,
|
||||
get_pos_profile_item_details_,
|
||||
)
|
||||
|
||||
if not doc.pos_profile and not doc.flags.ignore_pos_profile:
|
||||
pos_profile = get_pos_profile(doc.company) or {}
|
||||
if not pos_profile:
|
||||
return None
|
||||
doc.pos_profile = pos_profile.get("name")
|
||||
|
||||
pos = {}
|
||||
if doc.pos_profile:
|
||||
pos = frappe.get_doc("POS Profile", doc.pos_profile)
|
||||
|
||||
if pos:
|
||||
if not for_validate:
|
||||
update_multi_mode_option(doc, pos)
|
||||
doc.tax_category = pos.get("tax_category")
|
||||
|
||||
if not for_validate and not doc.customer:
|
||||
doc.customer = pos.customer
|
||||
|
||||
if not for_validate:
|
||||
doc.ignore_pricing_rule = pos.ignore_pricing_rule
|
||||
|
||||
if pos.get("account_for_change_amount"):
|
||||
doc.account_for_change_amount = pos.get("account_for_change_amount")
|
||||
|
||||
for fieldname in (
|
||||
"currency",
|
||||
"letter_head",
|
||||
"tc_name",
|
||||
"company",
|
||||
"select_print_heading",
|
||||
"write_off_account",
|
||||
"taxes_and_charges",
|
||||
"write_off_cost_center",
|
||||
"apply_discount_on",
|
||||
"cost_center",
|
||||
):
|
||||
if (not for_validate) or (for_validate and not doc.get(fieldname)):
|
||||
doc.set(fieldname, pos.get(fieldname))
|
||||
|
||||
if pos.get("company_address"):
|
||||
doc.company_address = pos.get("company_address")
|
||||
|
||||
if doc.customer:
|
||||
customer_price_list, customer_group = frappe.get_value(
|
||||
"Customer", doc.customer, ["default_price_list", "customer_group"]
|
||||
)
|
||||
customer_group_price_list = frappe.get_value(
|
||||
"Customer Group", customer_group, "default_price_list"
|
||||
)
|
||||
selling_price_list = (
|
||||
customer_price_list or customer_group_price_list or pos.get("selling_price_list")
|
||||
)
|
||||
else:
|
||||
selling_price_list = pos.get("selling_price_list")
|
||||
|
||||
if selling_price_list:
|
||||
doc.set("selling_price_list", selling_price_list)
|
||||
|
||||
if not for_validate:
|
||||
dn_flag = any(d.get("dn_detail") for d in doc.get("items"))
|
||||
doc.update_stock = 0 if dn_flag else cint(pos.get("update_stock"))
|
||||
|
||||
for item in doc.get("items"):
|
||||
if item.get("item_code"):
|
||||
profile_details = get_pos_profile_item_details_(
|
||||
ItemDetailsCtx(item.as_dict()), pos, pos, update_data=True
|
||||
)
|
||||
for fname, val in profile_details.items():
|
||||
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||
item.set(fname, val)
|
||||
|
||||
if doc.tc_name and not doc.terms:
|
||||
doc.terms = frappe.db.get_value("Terms and Conditions", doc.tc_name, "terms")
|
||||
|
||||
if doc.taxes_and_charges and not len(doc.get("taxes")):
|
||||
from erpnext.accounts.services.taxes import TaxService
|
||||
|
||||
TaxService(doc).set_taxes()
|
||||
|
||||
return pos
|
||||
|
||||
def set_paid_amount(self) -> None:
|
||||
doc = self.doc
|
||||
paid_amount = 0.0
|
||||
base_paid_amount = 0.0
|
||||
for data in doc.payments:
|
||||
data.base_amount = flt(data.amount * doc.conversion_rate, doc.precision("base_paid_amount"))
|
||||
paid_amount += data.amount
|
||||
base_paid_amount += data.base_amount
|
||||
doc.paid_amount = paid_amount
|
||||
doc.base_paid_amount = base_paid_amount
|
||||
|
||||
def set_account_for_mode_of_payment(self) -> None:
|
||||
for payment in self.doc.payments:
|
||||
payment.account = get_bank_cash_account(payment.mode_of_payment, self.doc.company).get("account")
|
||||
|
||||
def reset_mode_of_payments(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.pos_profile:
|
||||
pos_profile = frappe.get_cached_doc("POS Profile", doc.pos_profile)
|
||||
update_multi_mode_option(doc, pos_profile)
|
||||
doc.paid_amount = 0
|
||||
|
||||
def validate_pos_return(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.is_consolidated:
|
||||
return
|
||||
|
||||
if doc.is_pos and doc.is_return:
|
||||
total_amount_in_payments = sum(payment.amount for payment in doc.payments)
|
||||
invoice_total = doc.rounded_total or doc.grand_total
|
||||
if total_amount_in_payments < invoice_total:
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
def validate_pos_paid_amount(self) -> None:
|
||||
doc = self.doc
|
||||
if len(doc.payments) == 0 and doc.is_pos and flt(doc.grand_total) > 0:
|
||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||
|
||||
def validate_pos(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.is_return:
|
||||
invoice_total = doc.rounded_total or doc.grand_total
|
||||
if abs(flt(doc.paid_amount)) + abs(flt(doc.write_off_amount)) - abs(flt(invoice_total)) > 1.0 / (
|
||||
10.0 ** (doc.precision("grand_total") + 1.0)
|
||||
):
|
||||
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
||||
|
||||
def validate_created_using_pos(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.is_created_using_pos and not doc.pos_profile:
|
||||
frappe.throw(_("POS Profile is mandatory to mark this invoice as POS Transaction."))
|
||||
|
||||
doc.invoice_type_in_pos = frappe.db.get_single_value("POS Settings", "invoice_type")
|
||||
if doc.invoice_type_in_pos == "POS Invoice" and not doc.is_return:
|
||||
frappe.throw(_("Transactions using Sales Invoice in POS are disabled."))
|
||||
|
||||
self.validate_pos_opening_entry()
|
||||
|
||||
def validate_full_payment(self) -> None:
|
||||
doc = self.doc
|
||||
allow_partial_payment = frappe.db.get_value("POS Profile", doc.pos_profile, "allow_partial_payment")
|
||||
invoice_total = flt(doc.rounded_total) or flt(doc.grand_total)
|
||||
|
||||
if (
|
||||
doc.docstatus == 1
|
||||
and not doc.is_return
|
||||
and not allow_partial_payment
|
||||
and doc.paid_amount < invoice_total
|
||||
):
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
|
||||
def validate_pos_opening_entry(self) -> None:
|
||||
doc = self.doc
|
||||
opening_entries = frappe.get_all(
|
||||
"POS Opening Entry",
|
||||
fields=["name", "period_start_date"],
|
||||
filters={"pos_profile": doc.pos_profile, "status": "Open"},
|
||||
order_by="period_start_date desc",
|
||||
)
|
||||
if not opening_entries:
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Missing"),
|
||||
msg=_("No open POS Opening Entry found for POS Profile {0}.").format(
|
||||
frappe.bold(doc.pos_profile)
|
||||
),
|
||||
)
|
||||
if len(opening_entries) > 1:
|
||||
frappe.throw(
|
||||
title=_("Multiple POS Opening Entry"),
|
||||
msg=_(
|
||||
"POS Profile - {0} has multiple open POS Opening Entries. Please close or cancel the existing entries before proceeding."
|
||||
).format(doc.pos_profile),
|
||||
)
|
||||
if frappe.utils.get_date_str(opening_entries[0].get("period_start_date")) != frappe.utils.today():
|
||||
frappe.throw(
|
||||
title=_("Outdated POS Opening Entry"),
|
||||
msg=_(
|
||||
"POS Opening Entry - {0} is outdated. Please close the POS and create a new POS Opening Entry."
|
||||
).format(opening_entries[0].get("name")),
|
||||
)
|
||||
|
||||
def check_if_consolidated_invoice(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.doctype == "Sales Invoice" and doc.is_consolidated:
|
||||
invoice_or_credit_note = "consolidated_credit_note" if doc.is_return else "consolidated_invoice"
|
||||
pos_closing_entry = frappe.get_all(
|
||||
"POS Invoice Merge Log",
|
||||
filters={invoice_or_credit_note: doc.name},
|
||||
pluck="pos_closing_entry",
|
||||
)
|
||||
if pos_closing_entry and pos_closing_entry[0]:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold(_("Consolidated Sales Invoice")),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
|
||||
)
|
||||
frappe.throw(msg, title=_("Not Allowed"))
|
||||
|
||||
def check_if_created_using_pos_and_pos_closing_entry_generated(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.doctype == "Sales Invoice" and doc.is_created_using_pos and doc.pos_closing_entry:
|
||||
pos_closing_entry_docstatus = frappe.db.get_value(
|
||||
"POS Closing Entry", doc.pos_closing_entry, "docstatus"
|
||||
)
|
||||
if pos_closing_entry_docstatus == 1:
|
||||
frappe.throw(
|
||||
msg=_(
|
||||
"To cancel this Sales Invoice you need to cancel the POS Closing Entry {0}."
|
||||
).format(get_link_to_form("POS Closing Entry", doc.pos_closing_entry)),
|
||||
title=_("Not Allowed"),
|
||||
)
|
||||
|
||||
def cancel_pos_invoice_credit_note_generated_during_sales_invoice_mode(self) -> None:
|
||||
pos_invoices = frappe.get_all(
|
||||
"POS Invoice", filters={"consolidated_invoice": self.doc.name}, pluck="name"
|
||||
)
|
||||
for pos_invoice in pos_invoices:
|
||||
frappe.get_doc("POS Invoice", pos_invoice).cancel()
|
||||
|
||||
def clear_unallocated_mode_of_payments(self) -> None:
|
||||
doc = self.doc
|
||||
doc.set("payments", doc.get("payments", {"amount": ["not in", [0, None, ""]]}))
|
||||
frappe.db.delete("Sales Invoice Payment", filters={"parent": doc.name, "amount": 0})
|
||||
|
||||
def allow_write_off_only_on_pos(self) -> None:
|
||||
if not self.doc.is_pos and self.doc.write_off_account:
|
||||
self.doc.write_off_account = None
|
||||
|
||||
def verify_payment_amount_is_positive(self) -> None:
|
||||
for entry in self.doc.payments:
|
||||
if entry.amount < 0:
|
||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
|
||||
|
||||
def verify_payment_amount_is_negative(self) -> None:
|
||||
for entry in self.doc.payments:
|
||||
if entry.amount > 0:
|
||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
||||
|
||||
def get_warehouse(self) -> str | None:
|
||||
doc = self.doc
|
||||
POSProfile = frappe.qb.DocType("POS Profile")
|
||||
|
||||
user_query = (
|
||||
frappe.qb.from_(POSProfile)
|
||||
.select(POSProfile.name, POSProfile.warehouse)
|
||||
.where(POSProfile.company == doc.company)
|
||||
.where(
|
||||
(POSProfile.user == frappe.session["user"])
|
||||
| ((POSProfile.user.isnull() | (POSProfile.user == "")) & (frappe.session["user"] == ""))
|
||||
)
|
||||
)
|
||||
user_pos_profile = user_query.run()
|
||||
warehouse = user_pos_profile[0][1] if user_pos_profile else None
|
||||
|
||||
if not warehouse:
|
||||
global_query = (
|
||||
frappe.qb.from_(POSProfile)
|
||||
.select(POSProfile.name, POSProfile.warehouse)
|
||||
.where(POSProfile.company == doc.company)
|
||||
.where(POSProfile.user.isnull() | (POSProfile.user == ""))
|
||||
)
|
||||
global_pos_profile = global_query.run()
|
||||
|
||||
if global_pos_profile:
|
||||
warehouse = global_pos_profile[0][1]
|
||||
elif not user_pos_profile:
|
||||
msgprint(_("POS Profile required to make POS Entry"), raise_exception=True)
|
||||
|
||||
return warehouse
|
||||
|
||||
|
||||
def get_bank_cash_account(mode_of_payment: str, company: str) -> dict:
|
||||
account = frappe.db.get_value(
|
||||
"Mode of Payment Account",
|
||||
{"parent": mode_of_payment, "company": company},
|
||||
"default_account",
|
||||
)
|
||||
if not account:
|
||||
frappe.throw(
|
||||
_("Please set default Cash or Bank account in Mode of Payment {0}").format(
|
||||
get_link_to_form("Mode of Payment", mode_of_payment)
|
||||
),
|
||||
title=_("Missing Account"),
|
||||
)
|
||||
return {"account": account}
|
||||
|
||||
|
||||
def update_multi_mode_option(doc, pos_profile) -> None:
|
||||
def append_payment(payment_mode):
|
||||
payment = doc.append("payments", {})
|
||||
payment.default = payment_mode.default
|
||||
payment.mode_of_payment = payment_mode.mop
|
||||
payment.account = payment_mode.default_account
|
||||
payment.type = payment_mode.type
|
||||
|
||||
mop_refetched = bool(doc.payments) and not doc.is_created_using_pos
|
||||
|
||||
doc.set("payments", [])
|
||||
invalid_modes = []
|
||||
mode_of_payments = [d.mode_of_payment for d in pos_profile.get("payments")]
|
||||
mode_of_payments_info = get_mode_of_payments_info(mode_of_payments, doc.company)
|
||||
|
||||
for row in pos_profile.get("payments"):
|
||||
payment_mode = mode_of_payments_info.get(row.mode_of_payment)
|
||||
if not payment_mode:
|
||||
invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment))
|
||||
continue
|
||||
|
||||
payment_mode.default = row.default
|
||||
append_payment(payment_mode)
|
||||
|
||||
if invalid_modes:
|
||||
if invalid_modes == 1:
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
|
||||
else:
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
|
||||
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
|
||||
|
||||
if mop_refetched:
|
||||
frappe.toast(
|
||||
_("Payment methods refreshed. Please review before proceeding."),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
|
||||
def get_all_mode_of_payments(doc) -> list:
|
||||
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
|
||||
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(ModeOfPaymentAccount)
|
||||
.join(ModeOfPayment)
|
||||
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
|
||||
.select(
|
||||
ModeOfPaymentAccount.default_account, ModeOfPaymentAccount.parent, ModeOfPayment.type.as_("type")
|
||||
)
|
||||
.where(ModeOfPaymentAccount.company == doc.company)
|
||||
.where(ModeOfPayment.enabled == 1)
|
||||
)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def get_mode_of_payments_info(mode_of_payments: list, company: str) -> dict:
|
||||
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
|
||||
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(ModeOfPaymentAccount)
|
||||
.join(ModeOfPayment)
|
||||
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
|
||||
.select(
|
||||
ModeOfPaymentAccount.default_account,
|
||||
ModeOfPaymentAccount.parent.as_("mop"),
|
||||
ModeOfPayment.type.as_("type"),
|
||||
)
|
||||
.where(ModeOfPaymentAccount.company == company)
|
||||
.where(ModeOfPayment.enabled == 1)
|
||||
.where(ModeOfPayment.name.isin(mode_of_payments))
|
||||
.groupby(ModeOfPayment.name)
|
||||
)
|
||||
|
||||
data = query.run(as_dict=1)
|
||||
|
||||
return {row.get("mop"): row for row in data}
|
||||
|
||||
|
||||
def get_mode_of_payment_info(mode_of_payment: str, company: str) -> list:
|
||||
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
|
||||
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(ModeOfPayment)
|
||||
.join(ModeOfPaymentAccount)
|
||||
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
|
||||
.select(
|
||||
ModeOfPaymentAccount.default_account, ModeOfPaymentAccount.parent, ModeOfPayment.type.as_("type")
|
||||
)
|
||||
.where(ModeOfPaymentAccount.company == company)
|
||||
.where(ModeOfPayment.enabled == 1)
|
||||
.where(ModeOfPayment.name == mode_of_payment)
|
||||
)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
134
erpnext/accounts/doctype/sales_invoice/services/status.py
Normal file
134
erpnext/accounts/doctype/sales_invoice/services/status.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Status computation and display helpers for Sales Invoice."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, getdate, nowdate
|
||||
|
||||
|
||||
class StatusService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def set_status(
|
||||
self, update: bool = False, status: str | None = None, update_modified: bool = True
|
||||
) -> None:
|
||||
doc = self.doc
|
||||
if doc.is_new():
|
||||
if doc.get("amended_from"):
|
||||
doc.status = "Draft"
|
||||
return
|
||||
|
||||
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||
total = get_total_in_party_account_currency(doc)
|
||||
|
||||
if not status:
|
||||
if doc.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
elif doc.docstatus == 1:
|
||||
if doc.is_internal_transfer():
|
||||
doc.status = "Internal Transfer"
|
||||
elif is_overdue(doc, total):
|
||||
doc.status = "Overdue"
|
||||
elif 0 < outstanding_amount < total:
|
||||
doc.status = "Partly Paid"
|
||||
elif outstanding_amount > 0 and getdate(doc.due_date) >= getdate():
|
||||
doc.status = "Unpaid"
|
||||
elif doc.is_return == 0 and frappe.db.get_value(
|
||||
"Sales Invoice", {"is_return": 1, "return_against": doc.name, "docstatus": 1}
|
||||
):
|
||||
doc.status = "Credit Note Issued"
|
||||
elif doc.is_return == 1:
|
||||
doc.status = "Return"
|
||||
elif outstanding_amount <= 0:
|
||||
doc.status = "Paid"
|
||||
else:
|
||||
doc.status = "Submitted"
|
||||
|
||||
if (
|
||||
doc.status in ("Unpaid", "Partly Paid", "Overdue")
|
||||
and doc.is_discounted
|
||||
and get_discounting_status(doc.name) == "Disbursed"
|
||||
):
|
||||
doc.status += " and Discounted"
|
||||
|
||||
else:
|
||||
doc.status = "Draft"
|
||||
|
||||
if update:
|
||||
doc.db_set("status", doc.status, update_modified=update_modified)
|
||||
|
||||
def set_indicator(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.outstanding_amount < 0:
|
||||
doc.indicator_title = _("Credit Note Issued")
|
||||
doc.indicator_color = "gray"
|
||||
elif doc.outstanding_amount > 0 and getdate(doc.due_date) >= getdate(nowdate()):
|
||||
doc.indicator_color = "orange"
|
||||
doc.indicator_title = _("Unpaid")
|
||||
elif doc.outstanding_amount > 0 and getdate(doc.due_date) < getdate(nowdate()):
|
||||
doc.indicator_color = "red"
|
||||
doc.indicator_title = _("Overdue")
|
||||
elif cint(doc.is_return) == 1:
|
||||
doc.indicator_title = _("Return")
|
||||
doc.indicator_color = "gray"
|
||||
else:
|
||||
doc.indicator_color = "green"
|
||||
doc.indicator_title = _("Paid")
|
||||
|
||||
|
||||
def get_total_in_party_account_currency(doc) -> float:
|
||||
total_fieldname = "grand_total" if doc.disable_rounded_total else "rounded_total"
|
||||
if doc.party_account_currency != doc.currency:
|
||||
total_fieldname = "base_" + total_fieldname
|
||||
return flt(doc.get(total_fieldname), doc.precision(total_fieldname))
|
||||
|
||||
|
||||
def is_overdue(doc, total: float) -> bool | None:
|
||||
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||
if outstanding_amount <= 0:
|
||||
return
|
||||
|
||||
today = getdate()
|
||||
if doc.get("is_pos") or not doc.get("payment_schedule"):
|
||||
return getdate(doc.due_date) < today
|
||||
|
||||
payment_amount_field = (
|
||||
"base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount"
|
||||
)
|
||||
payable_amount = flt(
|
||||
sum(
|
||||
payment.get(payment_amount_field)
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < today
|
||||
),
|
||||
doc.precision("outstanding_amount"),
|
||||
)
|
||||
return flt(total - outstanding_amount, doc.precision("outstanding_amount")) < payable_amount
|
||||
|
||||
|
||||
def get_discounting_status(sales_invoice: str) -> str | None:
|
||||
status = None
|
||||
|
||||
InvoiceDiscounting = frappe.qb.DocType("Invoice Discounting")
|
||||
DiscountedInvoice = frappe.qb.DocType("Discounted Invoice")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(InvoiceDiscounting)
|
||||
.join(DiscountedInvoice)
|
||||
.on(InvoiceDiscounting.name == DiscountedInvoice.parent)
|
||||
.select(InvoiceDiscounting.status)
|
||||
.where(DiscountedInvoice.sales_invoice == sales_invoice)
|
||||
.where(InvoiceDiscounting.docstatus == 1)
|
||||
.where(InvoiceDiscounting.status.isin(["Disbursed", "Settled"]))
|
||||
)
|
||||
|
||||
invoice_discounting_list = query.run()
|
||||
|
||||
for d in invoice_discounting_list:
|
||||
status = d[0]
|
||||
if status == "Disbursed":
|
||||
break
|
||||
return status
|
||||
@@ -0,0 +1,121 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Timesheet billing helpers for Sales Invoice."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
||||
|
||||
|
||||
class TimesheetBillingService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def validate_time_sheets_are_submitted(self) -> None:
|
||||
for data in self.doc.timesheets:
|
||||
if data.time_sheet and data.timesheet_detail:
|
||||
if sales_invoice := frappe.db.get_value(
|
||||
"Timesheet Detail", data.timesheet_detail, "sales_invoice"
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Sales Invoice {1} is already created for {2}").format(
|
||||
data.idx, frappe.bold(sales_invoice), frappe.bold(data.time_sheet)
|
||||
)
|
||||
)
|
||||
|
||||
if data.time_sheet:
|
||||
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
|
||||
if status not in ["Submitted", "Payslip", "Partially Billed"]:
|
||||
frappe.throw(
|
||||
_("Timesheet {0} cannot be invoiced in its current state").format(data.time_sheet)
|
||||
)
|
||||
|
||||
def update_time_sheet(self, sales_invoice: str | None) -> None:
|
||||
for d in self.doc.timesheets:
|
||||
if d.time_sheet:
|
||||
timesheet = frappe.get_doc("Timesheet", d.time_sheet)
|
||||
self._update_time_sheet_detail(timesheet, d, sales_invoice)
|
||||
timesheet.calculate_total_amounts()
|
||||
timesheet.calculate_percentage_billed()
|
||||
timesheet.flags.ignore_validate_update_after_submit = True
|
||||
timesheet.set_status()
|
||||
timesheet.db_update_all()
|
||||
|
||||
def unlink_sales_invoice_from_timesheets(self) -> None:
|
||||
for row in self.doc.timesheets:
|
||||
timesheet = frappe.get_doc("Timesheet", row.time_sheet)
|
||||
timesheet.unlink_sales_invoice(self.doc.name)
|
||||
timesheet.flags.ignore_validate_update_after_submit = True
|
||||
timesheet.db_update_all()
|
||||
|
||||
def set_billing_hours_and_amount(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.project:
|
||||
return
|
||||
|
||||
for timesheet in doc.timesheets:
|
||||
ts_doc = frappe.get_doc("Timesheet", timesheet.time_sheet)
|
||||
if not timesheet.billing_hours and ts_doc.total_billable_hours:
|
||||
timesheet.billing_hours = ts_doc.total_billable_hours
|
||||
if not timesheet.billing_amount and ts_doc.total_billable_amount:
|
||||
timesheet.billing_amount = ts_doc.total_billable_amount
|
||||
|
||||
def update_timesheet_billing_for_project(self) -> None:
|
||||
doc = self.doc
|
||||
if (
|
||||
not doc.is_return
|
||||
and not doc.timesheets
|
||||
and doc.project
|
||||
and frappe.db.get_single_value("Projects Settings", "fetch_timesheet_in_sales_invoice")
|
||||
):
|
||||
self.add_timesheet_data()
|
||||
else:
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
def add_timesheet_data(self) -> None:
|
||||
doc = self.doc
|
||||
doc.set("timesheets", [])
|
||||
if doc.project:
|
||||
for data in get_projectwise_timesheet_data(doc.project):
|
||||
doc.append(
|
||||
"timesheets",
|
||||
{
|
||||
"time_sheet": data.time_sheet,
|
||||
"billing_hours": data.billing_hours,
|
||||
"billing_amount": data.billing_amount,
|
||||
"timesheet_detail": data.name,
|
||||
"activity_type": data.activity_type,
|
||||
"description": data.description,
|
||||
},
|
||||
)
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
def calculate_billing_amount_for_timesheet(self) -> None:
|
||||
doc = self.doc
|
||||
doc.total_billing_amount = sum(flt(ts.billing_amount) for ts in doc.timesheets)
|
||||
doc.total_billing_hours = sum(flt(ts.billing_hours) for ts in doc.timesheets)
|
||||
|
||||
def _update_time_sheet_detail(self, timesheet, args, sales_invoice: str | None) -> None:
|
||||
doc = self.doc
|
||||
for data in timesheet.time_logs:
|
||||
if (
|
||||
(doc.project and args.timesheet_detail == data.name)
|
||||
or (not doc.project and not data.sales_invoice and args.timesheet_detail == data.name)
|
||||
or (
|
||||
not sales_invoice
|
||||
and data.sales_invoice == doc.name
|
||||
and args.timesheet_detail == data.name
|
||||
)
|
||||
or (
|
||||
doc.is_return
|
||||
and doc.return_against
|
||||
and data.sales_invoice
|
||||
and data.sales_invoice == doc.return_against
|
||||
and not sales_invoice
|
||||
and args.timesheet_detail == data.name
|
||||
)
|
||||
):
|
||||
data.sales_invoice = sales_invoice
|
||||
@@ -19,7 +19,7 @@ from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import Warehouse
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
|
||||
unlink_payment_on_cancel_of_invoice,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_transaction
|
||||
from erpnext.accounts.utils import PaymentEntryUnlinkError
|
||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset
|
||||
@@ -30,7 +30,7 @@ from erpnext.controllers.accounts_controller import InvalidQtyError, update_invo
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
@@ -78,7 +78,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
def test_invalid_rate_without_override(self):
|
||||
from frappe import ValidationError
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_purchase_invoice
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Internal Customer 3", company="_Test Company", is_internal_customer=1, rate=100
|
||||
@@ -1278,7 +1278,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.validate_pos_gl_entry(si, pos, 50)
|
||||
|
||||
def test_pos_returns_with_repayment(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
|
||||
@@ -1397,7 +1397,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(pos.outstanding_amount, 0.0)
|
||||
self.assertEqual(pos.status, "Paid")
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
pos_return.save().submit()
|
||||
@@ -3777,6 +3777,14 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
si.submit()
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
|
||||
|
||||
def test_sales_invoice_cancellation_post_account_freezing_date(self):
|
||||
si = create_sales_invoice()
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", add_days(getdate(), 1))
|
||||
try:
|
||||
self.assertRaises(frappe.ValidationError, si.cancel)
|
||||
finally:
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
@@ -4200,7 +4208,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import (
|
||||
create_sales_invoice_record,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Set up loyalty program
|
||||
@@ -4338,7 +4346,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
from frappe.model.mapper import map_docs
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
method="erpnext.stock.doctype.delivery_note.mapper.make_sales_invoice",
|
||||
source_names=json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
@@ -4381,7 +4389,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pos_returns_without_update_outstanding_for_self(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.payments = []
|
||||
@@ -4751,7 +4759,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(project.total_billed_amount, 300)
|
||||
|
||||
def test_pos_returns_with_party_account_currency(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.payments = []
|
||||
|
||||
@@ -455,8 +455,10 @@ class Subscription(Document):
|
||||
tax_template = self.purchase_tax_template
|
||||
|
||||
if tax_template:
|
||||
from erpnext.accounts.services.taxes import TaxService
|
||||
|
||||
invoice.taxes_and_charges = tax_template
|
||||
invoice.set_taxes()
|
||||
TaxService(invoice).set_taxes()
|
||||
|
||||
# Due date
|
||||
if self.days_until_due:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule, get_tax_template
|
||||
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
|
||||
from erpnext.crm.doctype.opportunity.mapper import make_quotation
|
||||
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe.utils import add_days, add_months, getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import copy
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, now
|
||||
from frappe.utils.caching import request_cache
|
||||
|
||||
import erpnext
|
||||
@@ -18,11 +18,17 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.services.gl_validator import (
|
||||
check_freezing_date,
|
||||
validate_accounting_period,
|
||||
validate_against_pcv,
|
||||
validate_allowed_dimensions,
|
||||
validate_cwip_accounts,
|
||||
validate_disabled_accounts,
|
||||
)
|
||||
from erpnext.accounts.utils import create_payment_ledger_entry, is_immutable_ledger_enabled
|
||||
from erpnext.controllers.budget_controller import BudgetValidation
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
|
||||
def make_gl_entries(
|
||||
@@ -132,60 +138,6 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
|
||||
return accounting_dimensions_to_offset
|
||||
|
||||
|
||||
def validate_disabled_accounts(gl_map):
|
||||
accounts = [d.account for d in gl_map if d.account]
|
||||
|
||||
disabled_accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"disabled": 1, "is_group": 0, "company": gl_map[0].company},
|
||||
fields=["name"],
|
||||
)
|
||||
|
||||
used_disabled_accounts = set(accounts).intersection(set([d.name for d in disabled_accounts]))
|
||||
if used_disabled_accounts:
|
||||
account_list = "<br>"
|
||||
account_list += ", ".join([frappe.bold(d) for d in used_disabled_accounts])
|
||||
frappe.throw(
|
||||
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
|
||||
title=_("Disabled Account Selected"),
|
||||
)
|
||||
|
||||
|
||||
def validate_accounting_period(gl_map):
|
||||
accounting_periods = frappe.db.sql(
|
||||
""" SELECT
|
||||
ap.name as name, ap.exempted_role as exempted_role
|
||||
FROM
|
||||
`tabAccounting Period` ap, `tabClosed Document` cd
|
||||
WHERE
|
||||
ap.name = cd.parent
|
||||
AND ap.company = %(company)s
|
||||
AND ap.disabled = 0
|
||||
AND cd.closed = 1
|
||||
AND cd.document_type = %(voucher_type)s
|
||||
AND %(date)s between ap.start_date and ap.end_date
|
||||
""",
|
||||
{
|
||||
"date": gl_map[0].posting_date,
|
||||
"company": gl_map[0].company,
|
||||
"voucher_type": gl_map[0].voucher_type,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if accounting_periods:
|
||||
if accounting_periods[0].exempted_role:
|
||||
exempted_roles = accounting_periods[0].exempted_role
|
||||
if exempted_roles in frappe.get_roles():
|
||||
return
|
||||
frappe.throw(
|
||||
_(
|
||||
"You cannot create or cancel any accounting entries with in the closed Accounting Period {0}"
|
||||
).format(frappe.bold(accounting_periods[0].name)),
|
||||
ClosedAccountingPeriod,
|
||||
)
|
||||
|
||||
|
||||
def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False):
|
||||
if not gl_map:
|
||||
return []
|
||||
@@ -442,33 +394,6 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
validate_expense_against_budget(args)
|
||||
|
||||
|
||||
def validate_cwip_accounts(gl_map):
|
||||
"""Validate that CWIP account are not used in Journal Entry"""
|
||||
if gl_map and gl_map[0].voucher_type != "Journal Entry":
|
||||
return
|
||||
|
||||
cwip_enabled = any(
|
||||
cint(ac.enable_cwip_accounting)
|
||||
for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting")
|
||||
)
|
||||
if cwip_enabled:
|
||||
cwip_accounts = [
|
||||
d[0]
|
||||
for d in frappe.db.sql(
|
||||
"""select name from tabAccount
|
||||
where account_type = 'Capital Work in Progress' and is_group=0"""
|
||||
)
|
||||
]
|
||||
|
||||
for entry in gl_map:
|
||||
if entry.account in cwip_accounts:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry"
|
||||
).format(entry.account)
|
||||
)
|
||||
|
||||
|
||||
def process_debit_credit_difference(gl_map):
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("GL Entry").get_field("debit"),
|
||||
@@ -715,7 +640,7 @@ def make_reverse_gl_entries(
|
||||
partial_cancel=partial_cancel,
|
||||
)
|
||||
validate_accounting_period(gl_entries)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], gl_entries[0]["company"], adv_adj)
|
||||
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
||||
|
||||
@@ -796,48 +721,6 @@ def make_reverse_gl_entries(
|
||||
make_entry(new_gle, adv_adj, "Yes")
|
||||
|
||||
|
||||
def check_freezing_date(posting_date, company, adv_adj=False):
|
||||
"""
|
||||
Nobody can do GL Entries where posting date is before freezing date
|
||||
except authorized person
|
||||
|
||||
Administrator has all the roles so this check will be bypassed if any role is allowed to post
|
||||
Hence stop admin to bypass if accounts are freezed
|
||||
"""
|
||||
if not adv_adj:
|
||||
acc_frozen_till_date = frappe.db.get_value("Company", company, "accounts_frozen_till_date")
|
||||
if acc_frozen_till_date:
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if getdate(posting_date) <= getdate(acc_frozen_till_date) and (
|
||||
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
|
||||
):
|
||||
frappe.throw(
|
||||
_("You are not authorized to add or update entries before {0}").format(
|
||||
formatdate(acc_frozen_till_date)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def validate_against_pcv(is_opening, posting_date, company):
|
||||
if is_opening and frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}):
|
||||
frappe.throw(
|
||||
_("Opening Entry can not be created after Period Closing Voucher is created."),
|
||||
title=_("Invalid Opening Entry"),
|
||||
)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}]
|
||||
)
|
||||
|
||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||
message = _("Books have been closed till the period ending on {0}").format(formatdate(last_pcv_date))
|
||||
message += "</br >"
|
||||
message += _("You cannot create/amend any accounting entries till this date.")
|
||||
frappe.throw(message, title=_("Period Closed"))
|
||||
|
||||
|
||||
def set_as_cancel(voucher_type, voucher_no):
|
||||
"""
|
||||
Set is_cancelled=1 in all original gl entries for the voucher
|
||||
@@ -848,39 +731,3 @@ def set_as_cancel(voucher_type, voucher_no):
|
||||
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
|
||||
(now(), frappe.session.user, voucher_type, voucher_no),
|
||||
)
|
||||
|
||||
|
||||
def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
||||
for key, value in dimension_filter_map.items():
|
||||
dimension = key[0]
|
||||
account = key[1]
|
||||
|
||||
if gl_entry.account == account:
|
||||
if value["is_mandatory"] and not gl_entry.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(gl_entry.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(gl_entry.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(gl_entry.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(gl_entry.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
@@ -2,10 +2,10 @@ import frappe
|
||||
from frappe import qb
|
||||
from frappe.utils import add_days, flt, get_first_day, get_last_day, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note, make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_delivery_note, make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
@@ -354,7 +354,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
do_not_submit=False,
|
||||
)
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
from erpnext.selling.doctype.sales_order.mapper import (
|
||||
make_delivery_note,
|
||||
make_sales_invoice,
|
||||
)
|
||||
@@ -522,7 +522,7 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
do_not_submit=False,
|
||||
)
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
from erpnext.selling.doctype.sales_order.mapper import (
|
||||
make_delivery_note,
|
||||
make_sales_invoice,
|
||||
)
|
||||
@@ -732,8 +732,8 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
self.assertEqual(total[8], 100.0)
|
||||
|
||||
def test_drop_ship(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order, make_sales_invoice
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_purchase_order, make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
|
||||
0
erpnext/accounts/services/__init__.py
Normal file
0
erpnext/accounts/services/__init__.py
Normal file
510
erpnext/accounts/services/advances.py
Normal file
510
erpnext/accounts/services/advances.py
Normal file
@@ -0,0 +1,510 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Advance payment query and management functions.
|
||||
|
||||
All functions take a `doc` (AccountsController instance) as first argument so
|
||||
they can be called as module-level functions from any doctype, while keeping
|
||||
the AccountsController methods as thin shims.
|
||||
"""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_dimensions,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_advance_payment_doctypes
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
def set_advances(doc) -> None:
|
||||
"""Populate the advances child table from open advance entries."""
|
||||
res = get_advance_entries(
|
||||
doc, include_unallocated=not frappe.utils.cint(doc.get("only_include_allocated_payments"))
|
||||
)
|
||||
|
||||
doc.set("advances", [])
|
||||
advance_allocated = 0
|
||||
for d in res:
|
||||
if doc.get("party_account_currency") == doc.company_currency:
|
||||
amount = doc.get("base_rounded_total") or doc.base_grand_total
|
||||
else:
|
||||
amount = doc.get("rounded_total") or doc.grand_total
|
||||
allocated_amount = min(amount - advance_allocated, d.amount)
|
||||
advance_allocated += flt(allocated_amount)
|
||||
|
||||
advance_row = {
|
||||
"doctype": doc.doctype + " Advance",
|
||||
"reference_type": d.reference_type,
|
||||
"reference_name": d.reference_name,
|
||||
"reference_row": d.reference_row,
|
||||
"remarks": d.remarks,
|
||||
"advance_amount": flt(d.amount),
|
||||
"allocated_amount": allocated_amount,
|
||||
"ref_exchange_rate": flt(d.exchange_rate),
|
||||
"difference_posting_date": doc.posting_date,
|
||||
}
|
||||
if d.get("paid_from"):
|
||||
advance_row["account"] = d.paid_from
|
||||
if d.get("paid_to"):
|
||||
advance_row["account"] = d.paid_to
|
||||
|
||||
doc.append("advances", advance_row)
|
||||
|
||||
|
||||
def get_advance_entries(doc, include_unallocated: bool = True) -> list:
|
||||
"""Return advance journal and payment entries applicable to `doc`."""
|
||||
party_account = []
|
||||
default_advance_account = None
|
||||
|
||||
if doc.doctype in ["Sales Invoice", "POS Invoice"]:
|
||||
party_type = "Customer"
|
||||
party = doc.customer
|
||||
amount_field = "credit_in_account_currency"
|
||||
order_field = "sales_order"
|
||||
order_doctype = "Sales Order"
|
||||
party_account.append(doc.debit_to)
|
||||
else:
|
||||
party_type = "Supplier"
|
||||
party = doc.supplier
|
||||
amount_field = "debit_in_account_currency"
|
||||
order_field = "purchase_order"
|
||||
order_doctype = "Purchase Order"
|
||||
party_account.append(doc.credit_to)
|
||||
|
||||
party_accounts = get_party_account(party_type, party=party, company=doc.company, include_advance=True)
|
||||
|
||||
if party_accounts:
|
||||
party_account.append(party_accounts[0])
|
||||
default_advance_account = party_accounts[1] if len(party_accounts) == 2 else None
|
||||
|
||||
order_list = list(set(d.get(order_field) for d in doc.get("items") if d.get(order_field)))
|
||||
|
||||
journal_entries = get_advance_journal_entries(
|
||||
party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated
|
||||
)
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
party_type,
|
||||
party,
|
||||
party_account,
|
||||
order_doctype,
|
||||
order_list,
|
||||
default_advance_account,
|
||||
include_unallocated,
|
||||
)
|
||||
|
||||
return journal_entries + payment_entries
|
||||
|
||||
|
||||
def validate_advance_entries(doc) -> None:
|
||||
"""Warn if a payment entry linked to the same order is not pulled as advance."""
|
||||
order_field = "sales_order" if doc.doctype == "Sales Invoice" else "purchase_order"
|
||||
order_list = list(set(d.get(order_field) for d in doc.get("items") if d.get(order_field)))
|
||||
|
||||
if not order_list:
|
||||
return
|
||||
|
||||
advance_entries = get_advance_entries(doc, include_unallocated=False)
|
||||
|
||||
if advance_entries:
|
||||
advance_entries_against_si = [d.reference_name for d in doc.get("advances")]
|
||||
for d in advance_entries:
|
||||
if not advance_entries_against_si or d.reference_name not in advance_entries_against_si:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice."
|
||||
).format(d.reference_name, d.against_order)
|
||||
)
|
||||
|
||||
|
||||
def set_advance_gain_or_loss(doc) -> None:
|
||||
"""Compute exchange gain/loss for each allocated advance row."""
|
||||
if doc.get("conversion_rate") == 1 or not doc.get("advances"):
|
||||
return
|
||||
|
||||
is_purchase_invoice = doc.doctype == "Purchase Invoice"
|
||||
party_account = doc.credit_to if is_purchase_invoice else doc.debit_to
|
||||
if get_account_currency(party_account) != doc.currency:
|
||||
return
|
||||
|
||||
for d in doc.get("advances"):
|
||||
advance_exchange_rate = d.ref_exchange_rate
|
||||
if d.allocated_amount and doc.conversion_rate != advance_exchange_rate:
|
||||
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
|
||||
base_allocated_amount_in_inv_rate = doc.conversion_rate * d.allocated_amount
|
||||
difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate
|
||||
|
||||
d.exchange_gain_loss = difference
|
||||
|
||||
|
||||
def calculate_total_advance_from_ledger(doc) -> list:
|
||||
"""Query the Advance Payment Ledger for the total advance against `doc`."""
|
||||
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
||||
return (
|
||||
frappe.qb.from_(adv)
|
||||
.select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency"))
|
||||
.where(adv.company == doc.company)
|
||||
.where(adv.delinked == 0)
|
||||
.where(adv.against_voucher_type == doc.doctype)
|
||||
.where(adv.against_voucher_no == doc.name)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
def set_total_advance_paid(doc) -> None:
|
||||
"""Update advance_paid field and payment status from the ledger."""
|
||||
advance = calculate_total_advance_from_ledger(doc)
|
||||
advance_paid = 0
|
||||
|
||||
if advance:
|
||||
advance = advance[0]
|
||||
advance_paid = flt(advance.amount, doc.precision("advance_paid"))
|
||||
if advance.account_currency:
|
||||
frappe.db.set_value(doc.doctype, doc.name, "party_account_currency", advance.account_currency)
|
||||
|
||||
doc.db_set("advance_paid", advance_paid)
|
||||
set_advance_payment_status(doc)
|
||||
|
||||
|
||||
def set_advance_payment_status(doc) -> None:
|
||||
"""Sync advance_payment_status with current ledger and Payment Request state."""
|
||||
new_status = None
|
||||
|
||||
PaymentRequest = frappe.qb.DocType("Payment Request")
|
||||
paid_amount = frappe.get_value(
|
||||
doctype="Payment Request",
|
||||
filters={
|
||||
"reference_doctype": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname=Sum(PaymentRequest.grand_total - PaymentRequest.outstanding_amount),
|
||||
)
|
||||
|
||||
if not paid_amount:
|
||||
if doc.doctype in get_advance_payment_doctypes(payment_type="receivable"):
|
||||
new_status = "Not Requested" if paid_amount is None else "Requested"
|
||||
elif doc.doctype in get_advance_payment_doctypes(payment_type="payable"):
|
||||
new_status = "Not Initiated" if paid_amount is None else "Initiated"
|
||||
else:
|
||||
total_amount = doc.get("rounded_total") or doc.get("grand_total")
|
||||
new_status = "Fully Paid" if paid_amount == total_amount else "Partially Paid"
|
||||
|
||||
if new_status == doc.advance_payment_status:
|
||||
return
|
||||
|
||||
doc.db_set("advance_payment_status", new_status, update_modified=False)
|
||||
doc.set_status(update=True)
|
||||
doc.notify_update()
|
||||
|
||||
|
||||
def delink_advance_entries(doc, linked_doc_name: str) -> None:
|
||||
"""Remove advance rows linked to `linked_doc_name` and update total_advance."""
|
||||
total_allocated_amount = 0
|
||||
for adv in doc.advances:
|
||||
consider_for_total_advance = True
|
||||
if adv.reference_name == linked_doc_name:
|
||||
doctype = frappe.qb.DocType(doc.doctype + " Advance")
|
||||
frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run()
|
||||
|
||||
consider_for_total_advance = False
|
||||
|
||||
if consider_for_total_advance:
|
||||
total_allocated_amount += flt(adv.allocated_amount, adv.precision("allocated_amount"))
|
||||
|
||||
frappe.db.set_value(doc.doctype, doc.name, "total_advance", total_allocated_amount, update_modified=False)
|
||||
|
||||
|
||||
def create_advance_and_reconcile(doc, party_link) -> None:
|
||||
"""Create a Journal Entry to reconcile a party-link advance."""
|
||||
secondary_party_type, secondary_party = doc.get_party()
|
||||
primary_party_type, primary_party = party_link.primary_role, party_link.primary_party
|
||||
|
||||
primary_account = get_party_account(primary_party_type, primary_party, doc.company)
|
||||
secondary_account = get_party_account(secondary_party_type, secondary_party, doc.company)
|
||||
primary_account_currency = get_account_currency(primary_account)
|
||||
secondary_account_currency = get_account_currency(secondary_account)
|
||||
default_currency = erpnext.get_company_currency(doc.company)
|
||||
|
||||
multi_currency = (
|
||||
primary_account_currency != default_currency or secondary_account_currency != default_currency
|
||||
)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.voucher_type = "Journal Entry"
|
||||
jv.posting_date = doc.posting_date
|
||||
jv.company = doc.company
|
||||
jv.remark = f"Adjustment for {doc.doctype} {doc.name}"
|
||||
jv.is_system_generated = True
|
||||
|
||||
reconcilation_entry = frappe._dict()
|
||||
advance_entry = frappe._dict()
|
||||
|
||||
reconcilation_entry.account = secondary_account
|
||||
reconcilation_entry.party_type = secondary_party_type
|
||||
reconcilation_entry.party = secondary_party
|
||||
reconcilation_entry.reference_type = doc.doctype
|
||||
reconcilation_entry.reference_name = doc.name
|
||||
reconcilation_entry.cost_center = doc.cost_center or erpnext.get_default_cost_center(doc.company)
|
||||
|
||||
advance_entry.account = primary_account
|
||||
advance_entry.party_type = primary_party_type
|
||||
advance_entry.party = primary_party
|
||||
advance_entry.cost_center = doc.cost_center or erpnext.get_default_cost_center(doc.company)
|
||||
advance_entry.is_advance = "No" if doc.is_return else "Yes"
|
||||
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = doc.get(dim.fieldname)
|
||||
|
||||
reconcilation_entry.update(dimensions_dict)
|
||||
advance_entry.update(dimensions_dict)
|
||||
|
||||
if multi_currency:
|
||||
exc_rate_primary_to_default = (
|
||||
1
|
||||
if primary_account_currency == default_currency
|
||||
else get_exchange_rate(primary_account_currency, default_currency, doc.posting_date)
|
||||
)
|
||||
exc_rate_secondary_to_default = (
|
||||
1
|
||||
if secondary_account_currency == default_currency
|
||||
else get_exchange_rate(secondary_account_currency, default_currency, doc.posting_date)
|
||||
)
|
||||
exc_rate_secondary_to_primary = (
|
||||
1
|
||||
if secondary_account_currency == primary_account_currency
|
||||
else get_exchange_rate(secondary_account_currency, primary_account_currency, doc.posting_date)
|
||||
)
|
||||
|
||||
outstanding_amount = abs(doc.outstanding_amount)
|
||||
os_in_default_currency = outstanding_amount * exc_rate_secondary_to_default
|
||||
os_in_primary_currency = outstanding_amount * exc_rate_secondary_to_primary
|
||||
|
||||
reconciliation_is_credit = (doc.doctype == "Sales Invoice") != bool(doc.is_return)
|
||||
_set_je_amounts(
|
||||
reconcilation_entry, outstanding_amount, os_in_default_currency, reconciliation_is_credit
|
||||
)
|
||||
_set_je_amounts(
|
||||
advance_entry, os_in_primary_currency, os_in_default_currency, not reconciliation_is_credit
|
||||
)
|
||||
|
||||
reconcilation_entry.exchange_rate = exc_rate_secondary_to_default
|
||||
advance_entry.exchange_rate = exc_rate_primary_to_default
|
||||
else:
|
||||
outstanding_amount = abs(doc.outstanding_amount)
|
||||
reconciliation_is_credit = (doc.doctype == "Sales Invoice") != bool(doc.is_return)
|
||||
_set_je_amounts(reconcilation_entry, outstanding_amount, is_credit=reconciliation_is_credit)
|
||||
_set_je_amounts(advance_entry, outstanding_amount, is_credit=not reconciliation_is_credit)
|
||||
|
||||
jv.multi_currency = multi_currency
|
||||
jv.append("accounts", reconcilation_entry)
|
||||
jv.append("accounts", advance_entry)
|
||||
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
|
||||
def get_advance_journal_entries(
|
||||
party_type: str,
|
||||
party: str,
|
||||
party_account: list,
|
||||
amount_field: str,
|
||||
order_doctype: str,
|
||||
order_list: list,
|
||||
include_unallocated: bool = True,
|
||||
) -> list:
|
||||
"""Return open advance journal entry rows matching the given party and orders."""
|
||||
journal_entry = frappe.qb.DocType("Journal Entry")
|
||||
journal_acc = frappe.qb.DocType("Journal Entry Account")
|
||||
q = (
|
||||
frappe.qb.from_(journal_entry)
|
||||
.inner_join(journal_acc)
|
||||
.on(journal_entry.name == journal_acc.parent)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("reference_type"),
|
||||
(journal_entry.name).as_("reference_name"),
|
||||
(journal_entry.remark).as_("remarks"),
|
||||
(journal_acc[amount_field]).as_("amount"),
|
||||
(journal_acc.name).as_("reference_row"),
|
||||
(journal_acc.reference_name).as_("against_order"),
|
||||
(journal_acc.exchange_rate),
|
||||
)
|
||||
.where(
|
||||
journal_acc.account.isin(party_account)
|
||||
& (journal_acc.party_type == party_type)
|
||||
& (journal_acc.party == party)
|
||||
& (journal_acc.is_advance == "Yes")
|
||||
& (journal_entry.docstatus == 1)
|
||||
)
|
||||
)
|
||||
if party_type == "Customer":
|
||||
q = q.where(journal_acc.credit_in_account_currency > 0)
|
||||
else:
|
||||
q = q.where(journal_acc.debit_in_account_currency > 0)
|
||||
|
||||
reference_or_condition = []
|
||||
|
||||
if include_unallocated:
|
||||
reference_or_condition.append(journal_acc.reference_name.isnull())
|
||||
reference_or_condition.append(journal_acc.reference_name == "")
|
||||
|
||||
if order_list:
|
||||
reference_or_condition.append(
|
||||
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
|
||||
)
|
||||
|
||||
if reference_or_condition:
|
||||
q = q.where(Criterion.any(reference_or_condition))
|
||||
|
||||
q = q.orderby(journal_entry.posting_date)
|
||||
|
||||
return list(q.run(as_dict=True))
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_advance_payment_entries_for_regional(*args, **kwargs):
|
||||
return get_advance_payment_entries(*args, **kwargs)
|
||||
|
||||
|
||||
def get_advance_payment_entries(
|
||||
party_type: str,
|
||||
party: str,
|
||||
party_account: list,
|
||||
order_doctype: str,
|
||||
order_list: list | None = None,
|
||||
default_advance_account: str | None = None,
|
||||
include_unallocated: bool = True,
|
||||
against_all_orders: bool = False,
|
||||
limit: int | None = None,
|
||||
condition: dict | None = None,
|
||||
) -> list:
|
||||
"""Return open advance payment entry rows matching the given party and orders."""
|
||||
payment_entries = []
|
||||
payment_entry = frappe.qb.DocType("Payment Entry")
|
||||
|
||||
if order_list or against_all_orders:
|
||||
q = get_common_query(party_type, party, party_account, default_advance_account, limit, condition)
|
||||
payment_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||
|
||||
q = q.inner_join(payment_ref).on(payment_entry.name == payment_ref.parent)
|
||||
q = q.select(
|
||||
(payment_ref.allocated_amount).as_("amount"),
|
||||
(payment_ref.name).as_("reference_row"),
|
||||
(payment_ref.reference_name).as_("against_order"),
|
||||
(payment_entry.book_advance_payments_in_separate_party_account),
|
||||
)
|
||||
|
||||
q = q.where(payment_ref.reference_doctype == order_doctype)
|
||||
if order_list:
|
||||
q = q.where(payment_ref.reference_name.isin(order_list))
|
||||
|
||||
payment_entries += list(q.run(as_dict=True))
|
||||
|
||||
if include_unallocated:
|
||||
q = get_common_query(party_type, party, party_account, default_advance_account, limit, condition)
|
||||
q = q.select((payment_entry.unallocated_amount).as_("amount"))
|
||||
q = q.where(payment_entry.unallocated_amount > 0)
|
||||
|
||||
payment_entries += list(q.run(as_dict=True))
|
||||
|
||||
return payment_entries
|
||||
|
||||
|
||||
def get_common_query(
|
||||
party_type: str,
|
||||
party: str,
|
||||
party_account: list,
|
||||
default_advance_account: str | None,
|
||||
limit: int | None,
|
||||
condition: dict | None,
|
||||
):
|
||||
"""Build the base Payment Entry query shared by allocated and unallocated advance lookups."""
|
||||
account_type = frappe.db.get_value("Party Type", party_type, "account_type")
|
||||
payment_type = "Receive" if account_type == "Receivable" else "Pay"
|
||||
payment_entry = frappe.qb.DocType("Payment Entry")
|
||||
|
||||
q = (
|
||||
frappe.qb.from_(payment_entry)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("reference_type"),
|
||||
(payment_entry.name).as_("reference_name"),
|
||||
payment_entry.posting_date,
|
||||
(payment_entry.remarks).as_("remarks"),
|
||||
(payment_entry.book_advance_payments_in_separate_party_account),
|
||||
)
|
||||
.where(payment_entry.payment_type == payment_type)
|
||||
.where(payment_entry.party_type == party_type)
|
||||
.where(payment_entry.party == party)
|
||||
.where(payment_entry.docstatus == 1)
|
||||
)
|
||||
|
||||
field = "paid_from" if payment_type == "Receive" else "paid_to"
|
||||
q = q.select((payment_entry[f"{field}_account_currency"]).as_("currency"))
|
||||
q = q.select(payment_entry[field])
|
||||
account_condition = payment_entry[field].isin(party_account)
|
||||
if default_advance_account:
|
||||
q = q.where(
|
||||
account_condition
|
||||
| (
|
||||
(payment_entry[field] == default_advance_account)
|
||||
& (payment_entry.book_advance_payments_in_separate_party_account == 1)
|
||||
)
|
||||
)
|
||||
else:
|
||||
q = q.where(account_condition)
|
||||
|
||||
if payment_type == "Receive":
|
||||
q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate"))
|
||||
else:
|
||||
q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))
|
||||
|
||||
if condition:
|
||||
common_filter_conditions = []
|
||||
common_filter_conditions.append(payment_entry.company == condition["company"])
|
||||
if condition.get("name", None):
|
||||
common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%"))
|
||||
if condition.get("from_payment_date"):
|
||||
common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"]))
|
||||
if condition.get("to_payment_date"):
|
||||
common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"]))
|
||||
if condition.get("get_payments") is True:
|
||||
if condition.get("cost_center"):
|
||||
common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"])
|
||||
if condition.get("accounting_dimensions"):
|
||||
for field, val in condition.get("accounting_dimensions").items():
|
||||
common_filter_conditions.append(payment_entry[field] == val)
|
||||
if condition.get("minimum_payment_amount"):
|
||||
common_filter_conditions.append(
|
||||
payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"])
|
||||
)
|
||||
if condition.get("maximum_payment_amount"):
|
||||
common_filter_conditions.append(
|
||||
payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"])
|
||||
)
|
||||
q = q.where(Criterion.all(common_filter_conditions))
|
||||
|
||||
q = q.orderby(payment_entry.posting_date)
|
||||
q = q.limit(limit) if limit else q
|
||||
|
||||
return q
|
||||
|
||||
|
||||
def _set_je_amounts(entry, amount, default_amount=None, is_credit=True):
|
||||
if is_credit:
|
||||
entry.credit_in_account_currency = amount
|
||||
if default_amount is not None:
|
||||
entry.credit = default_amount
|
||||
else:
|
||||
entry.debit_in_account_currency = amount
|
||||
if default_amount is not None:
|
||||
entry.debit = default_amount
|
||||
269
erpnext/accounts/services/base_gl_composer.py
Normal file
269
erpnext/accounts/services/base_gl_composer.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Base class and free functions for per-document GL entry composition.
|
||||
|
||||
``BaseGLComposer`` holds the document being composed and exposes
|
||||
``get_gl_dict`` / ``add_gl_entry`` as instance methods. The underlying logic
|
||||
lives in the module-level free functions below (``doc`` as first argument), so
|
||||
``AccountsController`` and ``StockController`` can delegate to them via thin
|
||||
shims without forcing every GL-building doctype to inherit from those classes.
|
||||
|
||||
Subclasses implement ``compose`` to return the voucher-specific list of GL
|
||||
entries.
|
||||
"""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, formatdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||
from erpnext.accounts.services.taxes import set_balance_in_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_years
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
|
||||
def get_gl_dict(doc, args: dict, account_currency: str | None = None, item=None) -> dict:
|
||||
"""Build a GL entry dict populated with doc-level fields."""
|
||||
posting_date = args.get("posting_date") or doc.get("posting_date")
|
||||
fiscal_years = get_fiscal_years(posting_date, company=doc.company)
|
||||
if len(fiscal_years) > 1:
|
||||
frappe.throw(
|
||||
_("Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year").format(
|
||||
formatdate(posting_date)
|
||||
)
|
||||
)
|
||||
else:
|
||||
fiscal_year = fiscal_years[0][0]
|
||||
|
||||
gl_dict = frappe._dict(
|
||||
{
|
||||
"company": doc.company,
|
||||
"posting_date": posting_date,
|
||||
"fiscal_year": fiscal_year,
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"remarks": doc.get("remarks") or doc.get("remark"),
|
||||
"debit": 0,
|
||||
"credit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"is_opening": doc.get("is_opening") or "No",
|
||||
"party_type": None,
|
||||
"party": None,
|
||||
"project": doc.get("project"),
|
||||
"post_net_value": args.get("post_net_value"),
|
||||
"voucher_detail_no": args.get("voucher_detail_no"),
|
||||
"voucher_subtype": get_voucher_subtype(doc),
|
||||
}
|
||||
)
|
||||
|
||||
with temporary_flag("company", doc.company):
|
||||
update_gl_dict_with_regional_fields(doc, gl_dict)
|
||||
|
||||
update_gl_dict_with_app_based_fields(doc, gl_dict)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
dimension_dict = frappe._dict()
|
||||
for dimension in accounting_dimensions:
|
||||
dimension_dict[dimension] = doc.get(dimension)
|
||||
if item and item.get(dimension):
|
||||
dimension_dict[dimension] = item.get(dimension)
|
||||
|
||||
gl_dict.update(dimension_dict)
|
||||
gl_dict.update(args)
|
||||
|
||||
if not account_currency:
|
||||
account_currency = get_account_currency(gl_dict.account)
|
||||
|
||||
if gl_dict.account and doc.doctype not in [
|
||||
"Journal Entry",
|
||||
"Period Closing Voucher",
|
||||
"Payment Entry",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Stock Entry",
|
||||
]:
|
||||
validate_account_currency(doc, gl_dict.account, account_currency)
|
||||
|
||||
if gl_dict.account and doc.doctype not in [
|
||||
"Journal Entry",
|
||||
"Period Closing Voucher",
|
||||
"Payment Entry",
|
||||
]:
|
||||
set_balance_in_account_currency(
|
||||
gl_dict,
|
||||
account_currency,
|
||||
args.get("transaction_exchange_rate") or doc.get("conversion_rate"),
|
||||
doc.company_currency,
|
||||
)
|
||||
|
||||
if doc.doctype not in ["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"]:
|
||||
gl_dict.update(
|
||||
{
|
||||
"transaction_currency": doc.get("currency") or doc.company_currency,
|
||||
"transaction_exchange_rate": args.get("transaction_exchange_rate")
|
||||
or doc.get("conversion_rate", 1),
|
||||
"debit_in_transaction_currency": get_value_in_transaction_currency(
|
||||
doc, account_currency, gl_dict, "debit"
|
||||
),
|
||||
"credit_in_transaction_currency": get_value_in_transaction_currency(
|
||||
doc, account_currency, gl_dict, "credit"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if not args.get("against_voucher_type") and doc.get("against_voucher_type"):
|
||||
gl_dict.update({"against_voucher_type": doc.get("against_voucher_type")})
|
||||
|
||||
if not args.get("against_voucher") and doc.get("against_voucher"):
|
||||
gl_dict.update({"against_voucher": doc.get("against_voucher")})
|
||||
|
||||
return gl_dict
|
||||
|
||||
|
||||
def add_gl_entry(
|
||||
doc,
|
||||
gl_entries: list,
|
||||
account: str,
|
||||
cost_center: str,
|
||||
debit: float,
|
||||
credit: float,
|
||||
remarks: str,
|
||||
against_account: str,
|
||||
debit_in_account_currency: float | None = None,
|
||||
credit_in_account_currency: float | None = None,
|
||||
account_currency: str | None = None,
|
||||
project: str | None = None,
|
||||
voucher_detail_no: str | None = None,
|
||||
item=None,
|
||||
posting_date=None,
|
||||
) -> None:
|
||||
"""Build a GL entry via get_gl_dict and append it to gl_entries."""
|
||||
gl_entry = {
|
||||
"account": account,
|
||||
"cost_center": cost_center,
|
||||
"debit": debit,
|
||||
"credit": credit,
|
||||
"against": against_account,
|
||||
"remarks": remarks,
|
||||
}
|
||||
|
||||
if voucher_detail_no:
|
||||
gl_entry["voucher_detail_no"] = voucher_detail_no
|
||||
|
||||
if debit_in_account_currency:
|
||||
gl_entry["debit_in_account_currency"] = debit_in_account_currency
|
||||
|
||||
if credit_in_account_currency:
|
||||
gl_entry["credit_in_account_currency"] = credit_in_account_currency
|
||||
|
||||
if posting_date:
|
||||
gl_entry["posting_date"] = posting_date
|
||||
|
||||
gl_entries.append(get_gl_dict(doc, gl_entry, account_currency, item=item))
|
||||
|
||||
|
||||
def get_voucher_subtype(doc) -> str:
|
||||
voucher_subtypes = {
|
||||
"Journal Entry": "voucher_type",
|
||||
"Payment Entry": "payment_type",
|
||||
"Stock Entry": "stock_entry_type",
|
||||
"Asset Capitalization": "entry_type",
|
||||
}
|
||||
|
||||
for method_name in frappe.get_hooks("voucher_subtypes"):
|
||||
voucher_subtype = frappe.get_attr(method_name)(doc)
|
||||
if voucher_subtype:
|
||||
return voucher_subtype
|
||||
|
||||
if doc.doctype in voucher_subtypes:
|
||||
return doc.get(voucher_subtypes[doc.doctype])
|
||||
elif doc.doctype == "Purchase Receipt" and doc.is_return:
|
||||
return "Purchase Return"
|
||||
elif doc.doctype == "Delivery Note" and doc.is_return:
|
||||
return "Sales Return"
|
||||
elif doc.doctype == "Sales Invoice" and doc.is_return:
|
||||
return "Credit Note"
|
||||
elif doc.doctype == "Sales Invoice" and doc.is_debit_note:
|
||||
return "Debit Note"
|
||||
elif doc.doctype == "Purchase Invoice" and doc.is_return:
|
||||
return "Debit Note"
|
||||
|
||||
return doc.doctype
|
||||
|
||||
|
||||
def get_value_in_transaction_currency(doc, account_currency: str, gl_dict: dict, field: str) -> float:
|
||||
if account_currency == doc.get("currency"):
|
||||
return gl_dict.get(field + "_in_account_currency")
|
||||
return flt(gl_dict.get(field, 0) / doc.get("conversion_rate", 1))
|
||||
|
||||
|
||||
def validate_account_currency(doc, account: str, account_currency: str | None = None) -> None:
|
||||
valid_currency = [doc.company_currency]
|
||||
if doc.get("currency") and doc.currency != doc.company_currency:
|
||||
valid_currency.append(doc.currency)
|
||||
|
||||
if account_currency not in valid_currency:
|
||||
frappe.throw(
|
||||
_("Account {0} is invalid. Account Currency must be {1}").format(
|
||||
account, (" " + _("or") + " ").join(valid_currency)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def update_gl_dict_with_regional_fields(doc, gl_dict):
|
||||
pass
|
||||
|
||||
|
||||
def update_gl_dict_with_app_based_fields(doc, gl_dict):
|
||||
for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]):
|
||||
frappe.get_attr(method)(doc, gl_dict)
|
||||
|
||||
|
||||
class BaseGLComposer:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def compose(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_gl_dict(self, args: dict, account_currency: str | None = None, item=None) -> dict:
|
||||
return get_gl_dict(self.doc, args, account_currency, item)
|
||||
|
||||
def add_gl_entry(
|
||||
self,
|
||||
gl_entries: list,
|
||||
account: str,
|
||||
cost_center: str,
|
||||
debit: float,
|
||||
credit: float,
|
||||
remarks: str,
|
||||
against_account: str,
|
||||
debit_in_account_currency: float | None = None,
|
||||
credit_in_account_currency: float | None = None,
|
||||
account_currency: str | None = None,
|
||||
project: str | None = None,
|
||||
voucher_detail_no: str | None = None,
|
||||
item=None,
|
||||
posting_date=None,
|
||||
) -> None:
|
||||
add_gl_entry(
|
||||
self.doc,
|
||||
gl_entries,
|
||||
account,
|
||||
cost_center,
|
||||
debit,
|
||||
credit,
|
||||
remarks,
|
||||
against_account,
|
||||
debit_in_account_currency,
|
||||
credit_in_account_currency,
|
||||
account_currency,
|
||||
project,
|
||||
voucher_detail_no,
|
||||
item,
|
||||
posting_date,
|
||||
)
|
||||
151
erpnext/accounts/services/billing_validation.py
Normal file
151
erpnext/accounts/services/billing_validation.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Billing amount validation helpers (overbilling checks)."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt, fmt_money
|
||||
|
||||
|
||||
class BillingValidationService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def validate_multiple_billing(self, ref_dt: str, item_ref_dn: str, based_on: str) -> None:
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
|
||||
ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
|
||||
if not ref_wise_billed_amount:
|
||||
return
|
||||
|
||||
total_overbilled_amt = 0.0
|
||||
overbilled_items = []
|
||||
precision = self.doc.precision(based_on, "items")
|
||||
precision_allowance = 1 / (10**precision)
|
||||
|
||||
role_allowed_to_overbill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
|
||||
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
|
||||
|
||||
for row in ref_wise_billed_amount.values():
|
||||
total_billed_amt = row.billed_amt
|
||||
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
|
||||
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
|
||||
|
||||
if total_billed_amt < 0 and max_allowed_amt < 0:
|
||||
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
|
||||
|
||||
overbill_amt = total_billed_amt - max_allowed_amt
|
||||
row["max_allowed_amt"] = max_allowed_amt
|
||||
total_overbilled_amt += overbill_amt
|
||||
|
||||
if overbill_amt > precision_allowance and not is_overbilling_allowed:
|
||||
if self.doc.doctype != "Purchase Invoice" or not cint(
|
||||
frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
):
|
||||
overbilled_items.append(row)
|
||||
|
||||
if overbilled_items:
|
||||
self.throw_overbill_exception(overbilled_items, precision)
|
||||
|
||||
if is_overbilling_allowed and total_overbilled_amt > 0.1:
|
||||
frappe.msgprint(
|
||||
_("Overbilling of {} ignored because you have {} role.").format(
|
||||
total_overbilled_amt, role_allowed_to_overbill
|
||||
),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
def get_reference_wise_billed_amt(self, ref_dt: str, item_ref_dn: str, based_on: str) -> dict | None:
|
||||
"""Return sum of billed amounts per reference row, including previously submitted invoices."""
|
||||
reference_names = [d.get(item_ref_dn) for d in self.doc.items if d.get(item_ref_dn)]
|
||||
if not reference_names:
|
||||
return
|
||||
|
||||
precision = self.doc.precision(based_on, "items")
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
|
||||
|
||||
ref_wise_billed_amount = {}
|
||||
for item in self.doc.items:
|
||||
key = item.get(item_ref_dn)
|
||||
if not key:
|
||||
continue
|
||||
|
||||
ref_amt = flt(reference_details.get(key), precision)
|
||||
current_amount = flt(item.get(based_on), precision)
|
||||
|
||||
if not ref_amt:
|
||||
if current_amount:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
ref_wise_billed_amount.setdefault(
|
||||
key,
|
||||
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
|
||||
)
|
||||
ref_wise_billed_amount[key]["rows"].append(item.idx)
|
||||
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
|
||||
ref_wise_billed_amount[key]["billed_amt"] += current_amount
|
||||
if key in already_billed:
|
||||
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
|
||||
|
||||
return ref_wise_billed_amount
|
||||
|
||||
def get_billing_reference_details(
|
||||
self, reference_names: list, reference_doctype: str, based_on: str
|
||||
) -> frappe._dict:
|
||||
return frappe._dict(
|
||||
frappe.get_all(
|
||||
reference_doctype,
|
||||
filters={"name": ("in", reference_names)},
|
||||
fields=["name", based_on],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
def get_already_billed_amount(
|
||||
self, reference_names: list, item_ref_dn: str, based_on: str
|
||||
) -> frappe._dict:
|
||||
item_doctype = frappe.qb.DocType(self.doc.items[0].doctype)
|
||||
based_on_field = frappe.qb.Field(based_on)
|
||||
join_field = frappe.qb.Field(item_ref_dn)
|
||||
|
||||
return frappe._dict(
|
||||
(
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(join_field, Sum(based_on_field))
|
||||
.where(join_field.isin(reference_names))
|
||||
.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.doc.name))
|
||||
.groupby(join_field)
|
||||
).run()
|
||||
)
|
||||
|
||||
def throw_overbill_exception(self, overbilled_items: list, precision: int) -> None:
|
||||
message = (
|
||||
_("<p>Cannot overbill for the following Items:</p>")
|
||||
+ "<ul>"
|
||||
+ "".join(
|
||||
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
|
||||
frappe.bold(item.item_code),
|
||||
", ".join(str(x) for x in item.rows),
|
||||
frappe.bold(
|
||||
fmt_money(item.max_allowed_amt, precision=precision, currency=self.doc.currency)
|
||||
),
|
||||
)
|
||||
for item in overbilled_items
|
||||
)
|
||||
+ "</ul>"
|
||||
)
|
||||
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
|
||||
frappe.throw(_(message))
|
||||
593
erpnext/accounts/services/child_item_update.py
Normal file
593
erpnext/accounts/services/child_item_update.py
Normal file
@@ -0,0 +1,593 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Child item update service: ChildItemUpdater class and helpers for the update_child_qty_rate API."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.utils import flt, get_link_to_form, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||
from erpnext.buying.utils import update_last_purchase_rate
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
from erpnext.stock.get_item_details import (
|
||||
get_bin_details,
|
||||
get_conversion_factor,
|
||||
get_item_warehouse_,
|
||||
)
|
||||
|
||||
|
||||
class ChildItemUpdater:
|
||||
"""Validates and applies item-level edits on submitted orders and quotations."""
|
||||
|
||||
def __init__(self, parent_doctype: str, parent_doctype_name: str, child_docname: str = "items"):
|
||||
self.parent_doctype = parent_doctype
|
||||
self.parent_doctype_name = parent_doctype_name
|
||||
self.child_docname = child_docname
|
||||
self.parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
self.allow_zero_qty = get_allow_zero_qty(parent_doctype)
|
||||
self._ordered_items: dict | None = None
|
||||
self._purchased_items: dict | None = None
|
||||
|
||||
def update(self, trans_items: str) -> None:
|
||||
"""Process item additions, edits, and deletions from trans_items JSON."""
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
|
||||
from erpnext.selling.doctype.quotation.mapper import get_ordered_items
|
||||
|
||||
data = frappe.parse_json(trans_items)
|
||||
any_qty_changed = False
|
||||
items_added_or_removed = False
|
||||
any_conversion_factor_changed = False
|
||||
|
||||
self._check_permissions("write")
|
||||
|
||||
if self.parent_doctype == "Quotation":
|
||||
self._ordered_items = get_ordered_items(self.parent.name)
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data, self._ordered_items)
|
||||
elif self.parent_doctype == "Supplier Quotation":
|
||||
self._purchased_items = get_purchased_items(self.parent.name)
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data, self._purchased_items)
|
||||
else:
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data)
|
||||
|
||||
for d in data:
|
||||
new_child_flag = False
|
||||
rate_unchanged = None
|
||||
|
||||
if not d.get("item_code"):
|
||||
continue
|
||||
|
||||
if not d.get("docname"):
|
||||
new_child_flag = True
|
||||
items_added_or_removed = True
|
||||
self._check_permissions("create")
|
||||
child_item = self._get_new_child_item(d)
|
||||
else:
|
||||
self._check_permissions("write")
|
||||
child_item = frappe.get_doc(self.parent_doctype + " Item", d.get("docname"))
|
||||
|
||||
change_state = get_child_item_change_state(self.parent_doctype, child_item, d)
|
||||
rate_unchanged = change_state.rate_unchanged
|
||||
any_conversion_factor_changed |= not change_state.conversion_factor_unchanged
|
||||
if is_child_item_unchanged(change_state):
|
||||
continue
|
||||
|
||||
self._validate_quantity_and_rate(child_item, d, rate_unchanged)
|
||||
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
if self.parent.doctype in ("Sales Order", "Purchase Order") and self.parent.is_subcontracted:
|
||||
self._validate_fg_item_for_subcontracting(d, new_child_flag)
|
||||
child_item.fg_item_qty = flt(d["fg_item_qty"])
|
||||
if new_child_flag:
|
||||
child_item.fg_item = d["fg_item"]
|
||||
|
||||
child_item.qty = flt(d.get("qty"))
|
||||
child_item.description = d.get("description")
|
||||
update_child_item_rate_and_discount(
|
||||
self.parent_doctype, child_item, d, self.allow_zero_qty, rate_unchanged=rate_unchanged
|
||||
)
|
||||
update_child_item_uom_and_weight(child_item, d)
|
||||
|
||||
if d.get("delivery_date") and self.parent_doctype == "Sales Order":
|
||||
child_item.delivery_date = d.get("delivery_date")
|
||||
|
||||
if d.get("schedule_date") and self.parent_doctype == "Purchase Order":
|
||||
child_item.schedule_date = d.get("schedule_date")
|
||||
|
||||
if d.get("bom_no") and self.parent_doctype == "Sales Order":
|
||||
child_item.bom_no = d.get("bom_no")
|
||||
|
||||
child_item.flags.ignore_validate_update_after_submit = True
|
||||
if new_child_flag:
|
||||
self.parent.load_from_db()
|
||||
child_item.idx = len(self.parent.items) + 1
|
||||
child_item.insert()
|
||||
else:
|
||||
child_item.save(ignore_permissions=True)
|
||||
|
||||
self._post_update(any_qty_changed, items_added_or_removed, any_conversion_factor_changed)
|
||||
|
||||
def _post_update(
|
||||
self, any_qty_changed: bool, items_added_or_removed: bool, any_conversion_factor_changed: bool
|
||||
) -> None:
|
||||
parent = self.parent
|
||||
parent.reload()
|
||||
parent.flags.ignore_validate_update_after_submit = True
|
||||
parent.set_qty_as_per_stock_uom()
|
||||
parent.calculate_taxes_and_totals()
|
||||
parent.set_total_in_words()
|
||||
|
||||
if self.parent_doctype == "Sales Order" and not parent.is_subcontracted:
|
||||
make_packing_list(parent)
|
||||
parent.set_gross_profit()
|
||||
|
||||
frappe.get_cached_doc("Authorization Control").validate_approving_authority(
|
||||
parent.doctype, parent.company, parent.base_grand_total
|
||||
)
|
||||
|
||||
if self.parent_doctype != "Supplier Quotation":
|
||||
from erpnext.accounts.services.payment_schedule import PaymentScheduleService
|
||||
|
||||
PaymentScheduleService(parent).set_payment_schedule()
|
||||
|
||||
if self.parent_doctype == "Purchase Order":
|
||||
parent.validate_minimum_order_qty()
|
||||
parent.validate_budget()
|
||||
if parent.is_against_so():
|
||||
parent.update_status_updater()
|
||||
elif self.parent_doctype == "Sales Order":
|
||||
parent.check_credit_limit()
|
||||
|
||||
for idx, row in enumerate(parent.get(self.child_docname), start=1):
|
||||
row.idx = idx
|
||||
|
||||
parent.save()
|
||||
|
||||
if self.parent_doctype == "Purchase Order":
|
||||
update_last_purchase_rate(parent, is_submit=1)
|
||||
|
||||
if any_qty_changed or items_added_or_removed or any_conversion_factor_changed:
|
||||
parent.update_prevdoc_status()
|
||||
|
||||
parent.update_requested_qty()
|
||||
parent.update_ordered_qty()
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
|
||||
if parent.is_subcontracted and not parent.can_update_items():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
|
||||
elif self.parent_doctype == "Sales Order":
|
||||
if parent.is_subcontracted and not parent.can_update_items():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order."
|
||||
)
|
||||
)
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
parent.update_project()
|
||||
parent.update_prevdoc_status("submit")
|
||||
parent.update_delivery_status()
|
||||
|
||||
parent.reload()
|
||||
self._validate_workflow()
|
||||
|
||||
if self.parent_doctype in ("Purchase Order", "Sales Order"):
|
||||
parent.update_blanket_order()
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
|
||||
parent.validate_uom_is_integer("uom", "qty")
|
||||
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
|
||||
if self.parent_doctype == "Sales Order" and not parent.is_subcontracted:
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
cancel_stock_reservation_entries,
|
||||
has_reserved_stock,
|
||||
)
|
||||
|
||||
if has_reserved_stock(parent.doctype, parent.name):
|
||||
cancel_stock_reservation_entries(parent.doctype, parent.name)
|
||||
if parent.per_picked == 0:
|
||||
parent.create_stock_reservation_entries()
|
||||
|
||||
def _check_permissions(self, perm_type: str = "create") -> None:
|
||||
try:
|
||||
self.parent.check_permission(perm_type)
|
||||
except frappe.PermissionError:
|
||||
actions = {"create": "add", "write": "update"}
|
||||
frappe.throw(
|
||||
_("You do not have permissions to {} items in a {}.").format(
|
||||
actions[perm_type], self.parent_doctype
|
||||
),
|
||||
title=_("Insufficient Permissions"),
|
||||
)
|
||||
|
||||
def _validate_workflow(self) -> None:
|
||||
workflow = get_workflow_name(self.parent.doctype)
|
||||
if not workflow:
|
||||
return
|
||||
|
||||
workflow_doc = frappe.get_doc("Workflow", workflow)
|
||||
current_state = self.parent.get(workflow_doc.workflow_state_field)
|
||||
roles = frappe.get_roles()
|
||||
|
||||
transitions = [
|
||||
t.as_dict()
|
||||
for t in workflow_doc.transitions
|
||||
if t.next_state == current_state
|
||||
and t.allowed in roles
|
||||
and is_transition_condition_satisfied(t, self.parent)
|
||||
]
|
||||
|
||||
if not transitions:
|
||||
frappe.throw(
|
||||
_("You are not allowed to update as per the conditions set in {} Workflow.").format(
|
||||
get_link_to_form("Workflow", workflow)
|
||||
),
|
||||
title=_("Insufficient Permissions"),
|
||||
)
|
||||
|
||||
def _get_new_child_item(self, item_row) -> "frappe.model.document.Document":
|
||||
child_doctype = self.parent_doctype + " Item"
|
||||
return set_order_defaults(
|
||||
self.parent_doctype,
|
||||
self.parent_doctype_name,
|
||||
child_doctype,
|
||||
self.child_docname,
|
||||
item_row,
|
||||
)
|
||||
|
||||
def _validate_quantity_and_rate(self, child_item, new_data: dict, rate_unchanged: bool | None) -> None:
|
||||
if not flt(new_data.get("qty")) and not self.allow_zero_qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
|
||||
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||
),
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
qty_limits = {
|
||||
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")),
|
||||
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")),
|
||||
}
|
||||
|
||||
if self.parent_doctype in qty_limits:
|
||||
qty_field, error_message = qty_limits[self.parent_doctype]
|
||||
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
|
||||
frappe.throw(
|
||||
_("Row #{0}:").format(new_data.get("idx")) + error_message,
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
if self.parent_doctype not in ("Quotation", "Supplier Quotation"):
|
||||
return
|
||||
|
||||
items_map = self._ordered_items if self.parent_doctype == "Quotation" else self._purchased_items
|
||||
if not items_map:
|
||||
return
|
||||
|
||||
qty_to_check = items_map.get(child_item.name)
|
||||
if not qty_to_check:
|
||||
return
|
||||
|
||||
if not rate_unchanged:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot update rate as item {0} is already ordered or purchased against this quotation"
|
||||
).format(frappe.bold(new_data.get("item_code")))
|
||||
)
|
||||
|
||||
if flt(new_data.get("qty")) < qty_to_check:
|
||||
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||
|
||||
def _validate_fg_item_for_subcontracting(self, new_data: dict, is_new: bool) -> None:
|
||||
if is_new:
|
||||
if not new_data.get("fg_item"):
|
||||
frappe.throw(
|
||||
_("Finished Good Item is not specified for service item {0}").format(
|
||||
new_data["item_code"]
|
||||
)
|
||||
)
|
||||
|
||||
is_sub_contracted_item, default_bom = frappe.db.get_value(
|
||||
"Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"]
|
||||
)
|
||||
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"])
|
||||
)
|
||||
elif not default_bom:
|
||||
frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"]))
|
||||
|
||||
if not new_data.get("fg_item_qty"):
|
||||
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(
|
||||
parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items"
|
||||
) -> None:
|
||||
ChildItemUpdater(parent_doctype, parent_doctype_name, child_docname).update(trans_items)
|
||||
|
||||
|
||||
def set_order_defaults(
|
||||
parent_doctype: str,
|
||||
parent_doctype_name: str,
|
||||
child_doctype: str,
|
||||
child_docname: str,
|
||||
trans_item: dict,
|
||||
) -> "frappe.model.document.Document":
|
||||
"""Return a new child item populated with item master defaults."""
|
||||
from erpnext.accounts.services.taxes import add_taxes_from_tax_template, set_child_tax_template_and_map
|
||||
|
||||
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
child_item = frappe.new_doc(child_doctype, parent_doc=p_doc, parentfield=child_docname)
|
||||
item = frappe.get_doc("Item", trans_item.get("item_code"))
|
||||
|
||||
for field in ("item_code", "item_name", "description", "item_group", "weight_per_unit", "weight_uom"):
|
||||
child_item.update({field: item.get(field)})
|
||||
|
||||
date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
|
||||
child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
|
||||
child_item.stock_uom = item.stock_uom
|
||||
child_item.uom = trans_item.get("uom") or item.stock_uom
|
||||
child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True)
|
||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
||||
child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company")))
|
||||
|
||||
if child_doctype in ("Purchase Order Item", "Supplier Quotation Item"):
|
||||
child_item.base_rate = 1
|
||||
child_item.base_amount = 1
|
||||
|
||||
if child_doctype == "Sales Order Item":
|
||||
child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True)
|
||||
if not child_item.warehouse:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings."
|
||||
).format(frappe.bold(item.item_code))
|
||||
)
|
||||
|
||||
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||
add_taxes_from_tax_template(child_item, p_doc)
|
||||
return child_item
|
||||
|
||||
|
||||
def validate_child_on_delete(row, parent, ordered_item=None) -> None:
|
||||
"""Raise if a partially transacted child item is being deleted."""
|
||||
if parent.doctype == "Sales Order":
|
||||
if flt(row.delivered_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been delivered").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
if flt(row.work_order_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
if flt(row.ordered_qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order."
|
||||
).format(row.idx, row.item_code)
|
||||
)
|
||||
|
||||
if parent.doctype == "Purchase Order" and flt(row.received_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been received").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if parent.doctype in ("Purchase Order", "Sales Order") and flt(row.billed_amt):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if parent.doctype == "Quotation" and ordered_item and ordered_item.get(row.name):
|
||||
frappe.throw(_("Cannot delete an item which has been ordered"))
|
||||
|
||||
|
||||
def update_bin_on_delete(row, doctype: str) -> None:
|
||||
"""Update bin quantities after a child item row is deleted."""
|
||||
from erpnext.stock.stock_balance import (
|
||||
get_indented_qty,
|
||||
get_ordered_qty,
|
||||
get_reserved_qty,
|
||||
update_bin_qty,
|
||||
)
|
||||
|
||||
qty_dict = {}
|
||||
|
||||
if doctype == "Sales Order":
|
||||
qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse)
|
||||
else:
|
||||
if row.material_request_item:
|
||||
qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse)
|
||||
qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
|
||||
|
||||
if row.warehouse:
|
||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||
|
||||
|
||||
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
|
||||
"""Delete child rows not present in data; return True if any were removed."""
|
||||
updated_item_names = [d.get("docname") for d in data]
|
||||
deleted_children = [item for item in parent.items if item.name not in updated_item_names]
|
||||
|
||||
for d in deleted_children:
|
||||
validate_child_on_delete(d, parent, ordered_item)
|
||||
d.cancel()
|
||||
d.delete()
|
||||
|
||||
if parent.doctype == "Purchase Order":
|
||||
parent.update_ordered_qty_in_so_for_removed_items(deleted_children)
|
||||
|
||||
if parent.doctype not in ("Quotation", "Supplier Quotation"):
|
||||
parent.update_prevdoc_status()
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
return bool(deleted_children)
|
||||
|
||||
|
||||
def get_allow_zero_qty(parent_doctype: str) -> bool:
|
||||
if parent_doctype == "Sales Order":
|
||||
return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False
|
||||
if parent_doctype == "Purchase Order":
|
||||
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
|
||||
return False
|
||||
|
||||
|
||||
def get_child_item_change_state(parent_doctype: str, child_item, new_data) -> frappe._dict:
|
||||
prev_rate, new_rate = flt(child_item.get("rate")), flt(new_data.get("rate"))
|
||||
prev_qty, new_qty = flt(child_item.get("qty")), flt(new_data.get("qty"))
|
||||
prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(new_data.get("fg_item_qty"))
|
||||
prev_con_fac = flt(child_item.get("conversion_factor"))
|
||||
new_con_fac = flt(new_data.get("conversion_factor"))
|
||||
|
||||
if parent_doctype == "Sales Order":
|
||||
prev_date, new_date = child_item.get("delivery_date"), new_data.get("delivery_date")
|
||||
elif parent_doctype == "Purchase Order":
|
||||
prev_date, new_date = child_item.get("schedule_date"), new_data.get("schedule_date")
|
||||
else:
|
||||
prev_date, new_date = None, None
|
||||
|
||||
if parent_doctype in ("Quotation", "Supplier Quotation"):
|
||||
date_unchanged = False
|
||||
else:
|
||||
prev_date = getdate(prev_date) if prev_date else None
|
||||
new_date = getdate(new_date) if new_date else None
|
||||
date_unchanged = prev_date == new_date
|
||||
|
||||
return frappe._dict(
|
||||
rate_unchanged=prev_rate == new_rate,
|
||||
qty_unchanged=prev_qty == new_qty,
|
||||
fg_qty_unchanged=prev_fg_qty == new_fg_qty,
|
||||
uom_unchanged=child_item.get("uom") == new_data.get("uom"),
|
||||
conversion_factor_unchanged=prev_con_fac == new_con_fac,
|
||||
date_unchanged=date_unchanged,
|
||||
description_unchanged=child_item.get("description") == new_data.get("description"),
|
||||
)
|
||||
|
||||
|
||||
def is_child_item_unchanged(change_state: frappe._dict) -> bool:
|
||||
return (
|
||||
change_state.rate_unchanged
|
||||
and change_state.qty_unchanged
|
||||
and change_state.fg_qty_unchanged
|
||||
and change_state.conversion_factor_unchanged
|
||||
and change_state.uom_unchanged
|
||||
and change_state.date_unchanged
|
||||
and change_state.description_unchanged
|
||||
)
|
||||
|
||||
|
||||
def update_child_item_rate_and_discount(
|
||||
parent_doctype: str,
|
||||
child_item,
|
||||
new_data,
|
||||
allow_zero_qty: bool,
|
||||
rate_unchanged: bool | None = None,
|
||||
) -> None:
|
||||
rate_precision = child_item.precision("rate") or 2
|
||||
qty_precision = child_item.precision("qty") or 2
|
||||
|
||||
if rate_unchanged is None:
|
||||
rate_unchanged = flt(child_item.get("rate")) == flt(new_data.get("rate"))
|
||||
|
||||
if not rate_unchanged and not child_item.get("qty") and allow_zero_qty:
|
||||
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
||||
|
||||
row_rate = flt(new_data.get("rate"), rate_precision)
|
||||
|
||||
if parent_doctype in ("Purchase Order", "Sales Order"):
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(new_data.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
||||
).format(child_item.idx, child_item.item_code)
|
||||
)
|
||||
|
||||
child_item.rate = row_rate
|
||||
|
||||
if parent_doctype not in ("Sales Order", "Purchase Order") or not flt(child_item.price_list_rate):
|
||||
return
|
||||
|
||||
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
||||
child_item.discount_percentage = 0
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.discount_percentage = flt(
|
||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||
child_item.precision("discount_percentage"),
|
||||
)
|
||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
|
||||
|
||||
def update_child_item_uom_and_weight(child_item, new_data) -> None:
|
||||
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||
|
||||
if new_data.get("conversion_factor"):
|
||||
if child_item.stock_uom == child_item.uom:
|
||||
child_item.conversion_factor = 1
|
||||
else:
|
||||
child_item.conversion_factor = flt(new_data.get("conversion_factor"), conv_fac_precision)
|
||||
|
||||
if new_data.get("uom"):
|
||||
child_item.uom = new_data.get("uom")
|
||||
conversion_factor = flt(
|
||||
get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")
|
||||
)
|
||||
child_item.conversion_factor = (
|
||||
flt(new_data.get("conversion_factor"), conv_fac_precision) or conversion_factor
|
||||
)
|
||||
|
||||
if child_item.get("weight_per_unit"):
|
||||
child_item.total_weight = flt(
|
||||
child_item.weight_per_unit * child_item.qty * child_item.conversion_factor,
|
||||
child_item.precision("total_weight"),
|
||||
)
|
||||
|
||||
|
||||
def check_if_child_table_updated(
|
||||
child_table_before_update, child_table_after_update, fields_to_check
|
||||
) -> bool:
|
||||
"""Return True if any accounting-relevant field changed in a child table."""
|
||||
fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"]
|
||||
|
||||
for index, item in enumerate(child_table_before_update):
|
||||
for field in fields_to_check:
|
||||
if child_table_after_update[index].get(field) != item.get(field):
|
||||
return True
|
||||
|
||||
return False
|
||||
208
erpnext/accounts/services/exchange_gain_loss.py
Normal file
208
erpnext/accounts/services/exchange_gain_loss.py
Normal file
@@ -0,0 +1,208 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Exchange gain/loss journal helpers."""
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.utils import create_gain_loss_journal, get_currency_precision
|
||||
|
||||
|
||||
def gain_loss_journal_already_booked(
|
||||
gain_loss_account: str,
|
||||
exc_gain_loss: float,
|
||||
ref2_dt: str,
|
||||
ref2_dn: str,
|
||||
ref2_detail_no: str,
|
||||
) -> bool:
|
||||
"""Check if a gain/loss journal has already been booked for the given parameters."""
|
||||
if res := frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"account": gain_loss_account,
|
||||
"reference_type": ref2_dt,
|
||||
"reference_name": ref2_dn,
|
||||
"reference_detail_no": ref2_detail_no,
|
||||
},
|
||||
pluck="parent",
|
||||
):
|
||||
res = list({x for x in res})
|
||||
if exc_vouchers := frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={"name": ["in", res], "voucher_type": "Exchange Gain Or Loss"},
|
||||
fields=["voucher_type", "total_debit", "total_credit"],
|
||||
):
|
||||
booked_voucher = exc_vouchers[0]
|
||||
if (
|
||||
booked_voucher.total_debit == exc_gain_loss
|
||||
and booked_voucher.total_credit == exc_gain_loss
|
||||
and booked_voucher.voucher_type == "Exchange Gain Or Loss"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def make_exchange_gain_loss_journal(
|
||||
doc, args: dict | None = None, dimensions_dict: dict | None = None
|
||||
) -> None:
|
||||
"""Make Exchange Gain/Loss journal for Invoices and Payments."""
|
||||
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
||||
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
||||
if doc.docstatus != 1:
|
||||
return
|
||||
|
||||
if dimensions_dict is None:
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = doc.get(dim.fieldname)
|
||||
|
||||
if doc.get("doctype") == "Journal Entry":
|
||||
if args:
|
||||
precision = get_currency_precision()
|
||||
for arg in args:
|
||||
if (
|
||||
flt(arg.get("difference_amount", 0), precision) != 0
|
||||
or flt(arg.get("exchange_gain_loss", 0), precision) != 0
|
||||
) and arg.get("difference_account"):
|
||||
party_account = arg.get("account")
|
||||
gain_loss_account = arg.get("difference_account")
|
||||
difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss")
|
||||
if difference_amount > 0:
|
||||
dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit"
|
||||
else:
|
||||
dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit"
|
||||
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
if not gain_loss_journal_already_booked(
|
||||
gain_loss_account,
|
||||
difference_amount,
|
||||
doc.doctype,
|
||||
doc.name,
|
||||
arg.get("referenced_row"),
|
||||
):
|
||||
posting_date = arg.get("difference_posting_date") or frappe.db.get_value(
|
||||
arg.voucher_type, arg.voucher_no, "posting_date"
|
||||
)
|
||||
je = create_gain_loss_journal(
|
||||
doc.company,
|
||||
posting_date,
|
||||
arg.get("party_type"),
|
||||
arg.get("party"),
|
||||
party_account,
|
||||
gain_loss_account,
|
||||
difference_amount,
|
||||
dr_or_cr,
|
||||
reverse_dr_or_cr,
|
||||
arg.get("against_voucher_type"),
|
||||
arg.get("against_voucher"),
|
||||
arg.get("idx"),
|
||||
doc.doctype,
|
||||
doc.name,
|
||||
arg.get("referenced_row"),
|
||||
arg.get("cost_center"),
|
||||
dimensions_dict,
|
||||
arg.get("project"),
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
get_link_to_form("Journal Entry", je)
|
||||
)
|
||||
)
|
||||
|
||||
if doc.get("doctype") == "Payment Entry":
|
||||
gain_loss_to_book = [x for x in doc.references if x.exchange_gain_loss != 0]
|
||||
booked = []
|
||||
if gain_loss_to_book:
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
parents = (
|
||||
qb.from_(jea)
|
||||
.select(jea.parent)
|
||||
.where(
|
||||
(jea.reference_type == "Payment Entry")
|
||||
& (jea.reference_name == doc.name)
|
||||
& (jea.docstatus == 1)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
if parents:
|
||||
booked = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(jea.reference_type, jea.reference_name, jea.reference_detail_no)
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (je.name.isin(parents))
|
||||
& (je.voucher_type == "Exchange Gain or Loss")
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
for d in gain_loss_to_book:
|
||||
if d.exchange_gain_loss and ((d.reference_doctype, d.reference_name, str(d.idx)) not in booked):
|
||||
if doc.book_advance_payments_in_separate_party_account:
|
||||
party_account = d.account
|
||||
else:
|
||||
if doc.payment_type == "Receive":
|
||||
party_account = doc.paid_from
|
||||
elif doc.payment_type == "Pay":
|
||||
party_account = doc.paid_to
|
||||
|
||||
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
||||
|
||||
if is_payable_account(d.reference_doctype, party_account):
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gain_loss_account = frappe.get_cached_value(
|
||||
"Company", doc.company, "exchange_gain_loss_account"
|
||||
)
|
||||
je = create_gain_loss_journal(
|
||||
doc.company,
|
||||
args.get("difference_posting_date") if args else doc.posting_date,
|
||||
doc.party_type,
|
||||
doc.party,
|
||||
party_account,
|
||||
gain_loss_account,
|
||||
d.exchange_gain_loss,
|
||||
dr_or_cr,
|
||||
reverse_dr_or_cr,
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
d.idx,
|
||||
doc.doctype,
|
||||
doc.name,
|
||||
d.idx,
|
||||
doc.cost_center,
|
||||
dimensions_dict,
|
||||
doc.project,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
get_link_to_form("Journal Entry", je)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_payable_account(reference_doctype: str, account: str) -> bool:
|
||||
if reference_doctype == "Purchase Invoice" or (
|
||||
reference_doctype == "Journal Entry"
|
||||
and frappe.get_cached_value("Account", account, "account_type") == "Payable"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def set_transaction_currency_and_rate_in_gl_map(doc, gl_entries: list) -> None:
|
||||
for entry in gl_entries:
|
||||
entry["transaction_currency"] = doc.currency
|
||||
entry["transaction_exchange_rate"] = doc.get("conversion_rate") or 1
|
||||
176
erpnext/accounts/services/gl_validator.py
Normal file
176
erpnext/accounts/services/gl_validator.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""List-level validations for a GL map.
|
||||
|
||||
These functions assert that an assembled list of GL entries is legal to post —
|
||||
no disabled accounts, the period/freeze/PCV gates pass, dimensions are allowed.
|
||||
They do not mutate or repair the entries; balancing and round-off live with the
|
||||
posting sink in ``erpnext.accounts.general_ledger``.
|
||||
"""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, formatdate, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
|
||||
def validate_disabled_accounts(gl_map):
|
||||
accounts = [d.account for d in gl_map if d.account]
|
||||
|
||||
disabled_accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"disabled": 1, "is_group": 0, "company": gl_map[0].company},
|
||||
fields=["name"],
|
||||
)
|
||||
|
||||
used_disabled_accounts = set(accounts).intersection(set([d.name for d in disabled_accounts]))
|
||||
if used_disabled_accounts:
|
||||
account_list = "<br>"
|
||||
account_list += ", ".join([frappe.bold(d) for d in used_disabled_accounts])
|
||||
frappe.throw(
|
||||
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
|
||||
title=_("Disabled Account Selected"),
|
||||
)
|
||||
|
||||
|
||||
def validate_accounting_period(gl_map):
|
||||
accounting_periods = frappe.db.sql(
|
||||
""" SELECT
|
||||
ap.name as name, ap.exempted_role as exempted_role
|
||||
FROM
|
||||
`tabAccounting Period` ap, `tabClosed Document` cd
|
||||
WHERE
|
||||
ap.name = cd.parent
|
||||
AND ap.company = %(company)s
|
||||
AND ap.disabled = 0
|
||||
AND cd.closed = 1
|
||||
AND cd.document_type = %(voucher_type)s
|
||||
AND %(date)s between ap.start_date and ap.end_date
|
||||
""",
|
||||
{
|
||||
"date": gl_map[0].posting_date,
|
||||
"company": gl_map[0].company,
|
||||
"voucher_type": gl_map[0].voucher_type,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if accounting_periods:
|
||||
if accounting_periods[0].exempted_role:
|
||||
exempted_roles = accounting_periods[0].exempted_role
|
||||
if exempted_roles in frappe.get_roles():
|
||||
return
|
||||
frappe.throw(
|
||||
_(
|
||||
"You cannot create or cancel any accounting entries with in the closed Accounting Period {0}"
|
||||
).format(frappe.bold(accounting_periods[0].name)),
|
||||
ClosedAccountingPeriod,
|
||||
)
|
||||
|
||||
|
||||
def validate_cwip_accounts(gl_map):
|
||||
"""Validate that CWIP account are not used in Journal Entry"""
|
||||
if gl_map and gl_map[0].voucher_type != "Journal Entry":
|
||||
return
|
||||
|
||||
cwip_enabled = any(
|
||||
cint(ac.enable_cwip_accounting)
|
||||
for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting")
|
||||
)
|
||||
if cwip_enabled:
|
||||
cwip_accounts = [
|
||||
d[0]
|
||||
for d in frappe.db.sql(
|
||||
"""select name from tabAccount
|
||||
where account_type = 'Capital Work in Progress' and is_group=0"""
|
||||
)
|
||||
]
|
||||
|
||||
for entry in gl_map:
|
||||
if entry.account in cwip_accounts:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry"
|
||||
).format(entry.account)
|
||||
)
|
||||
|
||||
|
||||
def check_freezing_date(posting_date, company, adv_adj=False):
|
||||
"""
|
||||
Nobody can do GL Entries where posting date is before freezing date
|
||||
except authorized person
|
||||
|
||||
Administrator has all the roles so this check will be bypassed if any role is allowed to post
|
||||
Hence stop admin to bypass if accounts are freezed
|
||||
"""
|
||||
if not adv_adj:
|
||||
acc_frozen_till_date = frappe.db.get_value("Company", company, "accounts_frozen_till_date")
|
||||
if acc_frozen_till_date:
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if getdate(posting_date) <= getdate(acc_frozen_till_date) and (
|
||||
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
|
||||
):
|
||||
frappe.throw(
|
||||
_("You are not authorized to add or update entries before {0}").format(
|
||||
formatdate(acc_frozen_till_date)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def validate_against_pcv(is_opening, posting_date, company):
|
||||
if is_opening and frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}):
|
||||
frappe.throw(
|
||||
_("Opening Entry can not be created after Period Closing Voucher is created."),
|
||||
title=_("Invalid Opening Entry"),
|
||||
)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}]
|
||||
)
|
||||
|
||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||
message = _("Books have been closed till the period ending on {0}").format(formatdate(last_pcv_date))
|
||||
message += "</br >"
|
||||
message += _("You cannot create/amend any accounting entries till this date.")
|
||||
frappe.throw(message, title=_("Period Closed"))
|
||||
|
||||
|
||||
def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
||||
for key, value in dimension_filter_map.items():
|
||||
dimension = key[0]
|
||||
account = key[1]
|
||||
|
||||
if gl_entry.account == account:
|
||||
if value["is_mandatory"] and not gl_entry.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(gl_entry.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(gl_entry.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(gl_entry.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(gl_entry.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
196
erpnext/accounts/services/internal_transfer.py
Normal file
196
erpnext/accounts/services/internal_transfer.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Internal transfer helpers: InternalTransferService for inter-company transaction validation and setup."""
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
|
||||
class InternalTransferService:
|
||||
"""Handles validation and setup for inter-company / internal transfer transactions."""
|
||||
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def is_internal_transfer(self) -> bool:
|
||||
"""Return True if document is an internal transfer (internal party + same represents_company)."""
|
||||
doc = self.doc
|
||||
if doc.doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
||||
internal_party_field = "is_internal_customer"
|
||||
elif doc.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
|
||||
internal_party_field = "is_internal_supplier"
|
||||
else:
|
||||
return False
|
||||
|
||||
return bool(doc.get(internal_party_field) and doc.represents_company == doc.company)
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Run all inter-company validations and apply internal-transfer field overrides."""
|
||||
self.validate_reference()
|
||||
self.validate_transaction()
|
||||
self.disable_pricing_rule()
|
||||
self.disable_tax_included_prices()
|
||||
|
||||
def set_account(self) -> None:
|
||||
"""Set unrealized profit/loss account for internal transfers (SI/PI only)."""
|
||||
if not self.is_internal_transfer() or self.doc.unrealized_profit_loss_account:
|
||||
return
|
||||
|
||||
unrealized_profit_loss_account = frappe.get_cached_value(
|
||||
"Company", self.doc.company, "unrealized_profit_loss_account"
|
||||
)
|
||||
|
||||
if not unrealized_profit_loss_account:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}"
|
||||
).format(frappe.bold(self.doc.company))
|
||||
)
|
||||
|
||||
self.doc.unrealized_profit_loss_account = unrealized_profit_loss_account
|
||||
|
||||
def process_common_party_accounting(self) -> None:
|
||||
"""Auto-create and reconcile advance for common party links (called from on_submit)."""
|
||||
if self.doc.doctype not in ("Sales Invoice", "Purchase Invoice"):
|
||||
return
|
||||
|
||||
if frappe.get_single_value("Accounts Settings", "enable_common_party_accounting"):
|
||||
party_link = self.get_common_party_link()
|
||||
if party_link and self.doc.outstanding_amount:
|
||||
from erpnext.accounts.services.advances import create_advance_and_reconcile
|
||||
|
||||
create_advance_and_reconcile(self.doc, party_link)
|
||||
|
||||
def get_common_party_link(self) -> frappe._dict | None:
|
||||
party_type, party = self.doc.get_party()
|
||||
return frappe.db.get_value(
|
||||
doctype="Party Link",
|
||||
filters={"secondary_role": party_type, "secondary_party": party},
|
||||
fieldname=["primary_role", "primary_party"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
def validate_reference(self) -> None:
|
||||
if self.doc.get("is_return"):
|
||||
return
|
||||
if self.doc.doctype not in ("Purchase Invoice", "Purchase Receipt"):
|
||||
return
|
||||
if not self.is_internal_transfer():
|
||||
return
|
||||
|
||||
if not (
|
||||
self.doc.get("inter_company_reference")
|
||||
or self.doc.get("inter_company_invoice_reference")
|
||||
or self.doc.get("inter_company_order_reference")
|
||||
):
|
||||
msg = _("Internal Sale or Delivery Reference missing.")
|
||||
msg += _("Please create purchase from internal sale or delivery document itself")
|
||||
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
|
||||
|
||||
label = "Delivery Note Item" if self.doc.doctype == "Purchase Receipt" else "Sales Invoice Item"
|
||||
field = frappe.scrub(label)
|
||||
|
||||
for row in self.doc.get("items"):
|
||||
if not row.get(field):
|
||||
frappe.throw(
|
||||
_(f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"),
|
||||
title=_("Internal Transfer Reference Missing"),
|
||||
)
|
||||
|
||||
def validate_transaction(self) -> None:
|
||||
if not cint(frappe.get_single_value("Accounts Settings", "maintain_same_internal_transaction_rate")):
|
||||
return
|
||||
|
||||
applicable_doctypes = ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice")
|
||||
if self.doc.doctype not in applicable_doctypes:
|
||||
return
|
||||
if not (self.doc.get("is_internal_customer") or self.doc.get("is_internal_supplier")):
|
||||
return
|
||||
|
||||
self._validate_transaction_by_voucher_type()
|
||||
|
||||
def disable_pricing_rule(self) -> None:
|
||||
if not self.doc.get("ignore_pricing_rule") and self.is_internal_transfer():
|
||||
self.doc.ignore_pricing_rule = 1
|
||||
frappe.msgprint(
|
||||
_("Disabled pricing rules since this {} is an internal transfer").format(self.doc.doctype),
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def disable_tax_included_prices(self) -> None:
|
||||
if not self.is_internal_transfer():
|
||||
return
|
||||
|
||||
tax_updated = False
|
||||
for tax in self.doc.get("taxes"):
|
||||
if tax.get("included_in_print_rate"):
|
||||
tax.included_in_print_rate = 0
|
||||
tax_updated = True
|
||||
|
||||
if tax_updated:
|
||||
frappe.msgprint(
|
||||
_("Disabled tax included prices since this {} is an internal transfer").format(
|
||||
self.doc.doctype
|
||||
),
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def _validate_transaction_by_voucher_type(self) -> None:
|
||||
orders = ("Sales Order", "Purchase Order")
|
||||
invoices = ("Sales Invoice", "Purchase Invoice")
|
||||
|
||||
if self.doc.doctype in orders and self.doc.get("inter_company_order_reference"):
|
||||
linked_doctype = "Sales Order" if self.doc.doctype == "Purchase Order" else "Purchase Order"
|
||||
self._validate_line_items(
|
||||
linked_doctype,
|
||||
"sales_order" if linked_doctype == "Sales Order" else "purchase_order",
|
||||
"sales_order_item" if linked_doctype == "Sales Order" else "purchase_order_item",
|
||||
)
|
||||
elif self.doc.doctype in invoices and self.doc.get("inter_company_invoice_reference"):
|
||||
linked_doctype = "Sales Invoice" if self.doc.doctype == "Purchase Invoice" else "Purchase Invoice"
|
||||
self._validate_line_items(
|
||||
linked_doctype,
|
||||
"sales_invoice" if linked_doctype == "Sales Invoice" else "purchase_invoice",
|
||||
"sales_invoice_item" if linked_doctype == "Sales Invoice" else "purchase_invoice_item",
|
||||
)
|
||||
|
||||
def _validate_line_items(self, ref_dt: str, ref_dn_field: str, ref_link_field: str) -> None:
|
||||
action, role_allowed_to_override = frappe.get_cached_value(
|
||||
"Accounts Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
|
||||
)
|
||||
|
||||
reference_names = [d.get(ref_link_field) for d in self.doc.get("items") if d.get(ref_link_field)]
|
||||
reference_details = self.doc.get_reference_details(reference_names, ref_dt + " Item")
|
||||
|
||||
stop_actions = []
|
||||
|
||||
for d in self.doc.get("items"):
|
||||
if not d.get(ref_link_field):
|
||||
continue
|
||||
|
||||
ref_rate = reference_details.get(d.get(ref_link_field))
|
||||
if ref_rate is None or abs(flt(d.rate - ref_rate, d.precision("rate"))) < 0.01:
|
||||
continue
|
||||
|
||||
ref_name = (
|
||||
self.doc.inter_company_invoice_reference
|
||||
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
|
||||
else d.get(ref_dn_field)
|
||||
)
|
||||
msg = _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||
d.idx, ref_dt, ref_name, d.rate, ref_rate
|
||||
)
|
||||
|
||||
if action == "Stop":
|
||||
user_roles = frappe.get_all(
|
||||
"Has Role", filters={"parent": frappe.session.user}, fields=["role"], pluck="role"
|
||||
)
|
||||
if role_allowed_to_override not in user_roles:
|
||||
stop_actions.append(msg)
|
||||
else:
|
||||
frappe.msgprint(msg, title=_("Warning"), indicator="orange")
|
||||
|
||||
if stop_actions:
|
||||
frappe.throw(stop_actions, as_list=True)
|
||||
223
erpnext/accounts/services/party_validation.py
Normal file
223
erpnext/accounts/services/party_validation.py
Normal file
@@ -0,0 +1,223 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Party validation: PartyValidator class for transaction-level party checks."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.accounts.party import (
|
||||
get_party_account_currency,
|
||||
get_party_gle_currency,
|
||||
validate_party_frozen_disabled,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
|
||||
|
||||
class PartyValidator:
|
||||
"""Validates all party-related fields on a transaction document."""
|
||||
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Run all party-related validations in order."""
|
||||
self.validate_party()
|
||||
self.validate_party_accounts()
|
||||
self.validate_currency()
|
||||
self.validate_party_account_currency()
|
||||
self.validate_address_and_contact()
|
||||
self.validate_company_linked_addresses()
|
||||
|
||||
def get_party(self) -> tuple[str | None, str | None]:
|
||||
"""Return (party_type, party_name) for the document."""
|
||||
doc = self.doc
|
||||
party_type = None
|
||||
|
||||
if doc.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
party_type = "Customer"
|
||||
elif doc.doctype in (
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
):
|
||||
party_type = "Supplier"
|
||||
elif doc.meta.get_field("customer"):
|
||||
party_type = "Customer"
|
||||
elif doc.meta.get_field("supplier"):
|
||||
party_type = "Supplier"
|
||||
|
||||
party = doc.get(party_type.lower()) if party_type else None
|
||||
return party_type, party
|
||||
|
||||
def validate_party(self) -> None:
|
||||
party_type, party = self.get_party()
|
||||
validate_party_frozen_disabled(self.doc.company, party_type, party)
|
||||
|
||||
def validate_party_accounts(self) -> None:
|
||||
if self.doc.doctype not in ("Sales Invoice", "Purchase Invoice"):
|
||||
return
|
||||
|
||||
if self.doc.doctype == "Sales Invoice":
|
||||
party_account_field = "debit_to"
|
||||
item_field = "income_account"
|
||||
else:
|
||||
party_account_field = "credit_to"
|
||||
item_field = "expense_account"
|
||||
|
||||
for item in self.doc.get("items"):
|
||||
if item.get(item_field) == self.doc.get(party_account_field):
|
||||
frappe.throw(
|
||||
_("Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}").format(
|
||||
item.idx,
|
||||
frappe.bold(frappe.unscrub(item_field)),
|
||||
item.get(item_field),
|
||||
frappe.bold(frappe.unscrub(party_account_field)),
|
||||
self.doc.get(party_account_field),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_currency(self) -> None:
|
||||
if not self.doc.get("currency"):
|
||||
return
|
||||
|
||||
party_type, party = self.get_party()
|
||||
if not (party_type and party):
|
||||
return
|
||||
|
||||
party_account_currency = get_party_account_currency(party_type, party, self.doc.company)
|
||||
|
||||
if (
|
||||
party_account_currency
|
||||
and party_account_currency != self.doc.company_currency
|
||||
and self.doc.currency != party_account_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_("Accounting Entry for {0}: {1} can only be made in currency: {2}").format(
|
||||
party_type, party, party_account_currency
|
||||
),
|
||||
InvalidCurrency,
|
||||
)
|
||||
|
||||
def validate_party_account_currency(self) -> None:
|
||||
if self.doc.doctype not in ("Sales Invoice", "Purchase Invoice"):
|
||||
return
|
||||
if self.doc.is_opening == "Yes":
|
||||
return
|
||||
|
||||
party_type, party = self.get_party()
|
||||
party_gle_currency = get_party_gle_currency(party_type, party, self.doc.company)
|
||||
party_account = (
|
||||
self.doc.get("debit_to") if self.doc.doctype == "Sales Invoice" else self.doc.get("credit_to")
|
||||
)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
allow_multi_currency = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "allow_multi_currency_invoices_against_single_party_account"
|
||||
)
|
||||
|
||||
if (
|
||||
not party_gle_currency
|
||||
and party_account_currency != self.doc.currency
|
||||
and not allow_multi_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
||||
frappe.bold(party_account), party_account_currency, self.doc.currency
|
||||
)
|
||||
)
|
||||
|
||||
def validate_address_and_contact(self) -> None:
|
||||
party_type, party = self.get_party()
|
||||
if not (party_type and party):
|
||||
return
|
||||
|
||||
if party_type == "Customer":
|
||||
self._validate_address(
|
||||
party,
|
||||
party_type,
|
||||
self.doc.get("customer_address"),
|
||||
self.doc.get("shipping_address_name"),
|
||||
)
|
||||
elif party_type == "Supplier":
|
||||
self._validate_address(party, party_type, self.doc.get("supplier_address"))
|
||||
|
||||
self._validate_contact(party, party_type)
|
||||
|
||||
def validate_company_linked_addresses(self) -> None:
|
||||
doc = self.doc
|
||||
sales_doctypes = ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice")
|
||||
purchase_doctypes = ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation")
|
||||
|
||||
if doc.doctype in sales_doctypes:
|
||||
address_fields = ["dispatch_address_name", "company_address"]
|
||||
elif doc.doctype in purchase_doctypes:
|
||||
address_fields = ["billing_address", "shipping_address"]
|
||||
else:
|
||||
return
|
||||
|
||||
is_drop_ship = (
|
||||
doc.doctype
|
||||
in {
|
||||
"Purchase Order",
|
||||
"Purchase Invoice",
|
||||
"Sales Order",
|
||||
"Sales Invoice",
|
||||
}
|
||||
and self._is_drop_ship()
|
||||
)
|
||||
|
||||
for field in address_fields:
|
||||
address = doc.get(field)
|
||||
if field in ("dispatch_address_name", "shipping_address") and is_drop_ship:
|
||||
continue
|
||||
if address and not frappe.db.exists(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"parent": address,
|
||||
"parenttype": "Address",
|
||||
"link_doctype": "Company",
|
||||
"link_name": doc.company,
|
||||
},
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} does not belong to the Company {1}.").format(
|
||||
_(doc.meta.get_label(field)), frappe.bold(doc.company)
|
||||
)
|
||||
)
|
||||
|
||||
def _validate_address(
|
||||
self,
|
||||
party: str,
|
||||
party_type: str,
|
||||
billing_address: str | None,
|
||||
shipping_address: str | None = None,
|
||||
) -> None:
|
||||
if not (billing_address or shipping_address):
|
||||
return
|
||||
|
||||
party_addresses = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
{"link_doctype": party_type, "link_name": party, "parenttype": "Address"},
|
||||
pluck="parent",
|
||||
)
|
||||
if billing_address and billing_address not in party_addresses:
|
||||
frappe.throw(_("Billing Address does not belong to the {0}").format(party))
|
||||
elif shipping_address and shipping_address not in party_addresses:
|
||||
frappe.throw(_("Shipping Address does not belong to the {0}").format(party))
|
||||
|
||||
def _validate_contact(self, party: str, party_type: str) -> None:
|
||||
if not self.doc.get("contact_person"):
|
||||
return
|
||||
|
||||
contacts = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
{"link_doctype": party_type, "link_name": party, "parenttype": "Contact"},
|
||||
pluck="parent",
|
||||
)
|
||||
if self.doc.contact_person not in contacts:
|
||||
frappe.throw(_("Contact Person does not belong to the {0}").format(party))
|
||||
|
||||
def _is_drop_ship(self) -> bool:
|
||||
return any(item.delivered_by_supplier for item in self.doc.items)
|
||||
391
erpnext/accounts/services/payment_schedule.py
Normal file
391
erpnext/accounts/services/payment_schedule.py
Normal file
@@ -0,0 +1,391 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Payment schedule and payment terms helpers."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import DateTimeLikeObject, add_days, add_months, cint, flt, get_last_day, getdate
|
||||
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
|
||||
|
||||
class PaymentScheduleService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def set_payment_schedule(self) -> None:
|
||||
doc = self.doc
|
||||
if (doc.doctype == "Sales Invoice" and doc.is_pos) or doc.get("is_opening") == "Yes":
|
||||
doc.payment_terms_template = ""
|
||||
return
|
||||
|
||||
party_account_currency = doc.get("party_account_currency")
|
||||
if not party_account_currency:
|
||||
party_type, party = doc.get_party()
|
||||
if party_type and party:
|
||||
party_account_currency = get_party_account_currency(party_type, party, doc.company)
|
||||
|
||||
posting_date = doc.get("bill_date") or doc.get("posting_date") or doc.get("transaction_date")
|
||||
due_date = doc.get("due_date") or posting_date
|
||||
|
||||
base_grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
||||
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
|
||||
automatically_fetch_payment_terms = 0
|
||||
|
||||
if doc.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
|
||||
po_or_so, doctype, fieldname = self.get_order_details()
|
||||
automatically_fetch_payment_terms = cint(
|
||||
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
|
||||
)
|
||||
if doc.doctype != "Sales Order":
|
||||
base_grand_total = base_grand_total - flt(doc.base_write_off_amount)
|
||||
grand_total = grand_total - flt(doc.write_off_amount)
|
||||
|
||||
if doc.get("total_advance"):
|
||||
if party_account_currency == doc.company_currency:
|
||||
base_grand_total -= doc.get("total_advance")
|
||||
grand_total = flt(base_grand_total / doc.get("conversion_rate"), doc.precision("grand_total"))
|
||||
else:
|
||||
grand_total -= doc.get("total_advance")
|
||||
base_grand_total = flt(
|
||||
grand_total * doc.get("conversion_rate"), doc.precision("base_grand_total")
|
||||
)
|
||||
|
||||
if not doc.get("payment_schedule"):
|
||||
if (
|
||||
doc.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
|
||||
and automatically_fetch_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
)
|
||||
if doc.get("payment_terms_template"):
|
||||
doc.ignore_default_payment_terms_template = 1
|
||||
elif doc.get("payment_terms_template"):
|
||||
data = get_payment_terms(
|
||||
doc.payment_terms_template, posting_date, grand_total, base_grand_total
|
||||
)
|
||||
for item in data:
|
||||
doc.append("payment_schedule", item)
|
||||
elif doc.doctype not in ["Purchase Receipt"]:
|
||||
doc.append(
|
||||
"payment_schedule",
|
||||
dict(
|
||||
due_date=due_date,
|
||||
invoice_portion=100,
|
||||
payment_amount=grand_total,
|
||||
base_payment_amount=base_grand_total,
|
||||
),
|
||||
)
|
||||
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template",
|
||||
doc.payment_terms_template,
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
)
|
||||
|
||||
if not (
|
||||
automatically_fetch_payment_terms
|
||||
and allocate_payment_based_on_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
for d in doc.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(
|
||||
grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
|
||||
)
|
||||
d.base_payment_amount = flt(
|
||||
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
d.base_outstanding = d.base_payment_amount
|
||||
elif not d.invoice_portion:
|
||||
d.base_payment_amount = flt(
|
||||
d.payment_amount * doc.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
d.base_outstanding = d.base_payment_amount
|
||||
else:
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
)
|
||||
doc.ignore_default_payment_terms_template = 1
|
||||
|
||||
def get_order_details(self) -> tuple:
|
||||
doc = self.doc
|
||||
if not doc.get("items"):
|
||||
return None, None, None
|
||||
if doc.doctype == "Sales Invoice":
|
||||
prev_doc = doc.get("items")[0].get("sales_order")
|
||||
prev_doctype = "Sales Order"
|
||||
prev_doctype_name = "sales_order"
|
||||
elif doc.doctype == "Purchase Invoice":
|
||||
prev_doc = doc.get("items")[0].get("purchase_order")
|
||||
prev_doctype = "Purchase Order"
|
||||
prev_doctype_name = "purchase_order"
|
||||
else:
|
||||
prev_doc = doc.get("items")[0].get("prevdoc_docname")
|
||||
prev_doctype = "Quotation"
|
||||
prev_doctype_name = "prevdoc_docname"
|
||||
return prev_doc, prev_doctype, prev_doctype_name
|
||||
|
||||
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype) -> bool:
|
||||
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
|
||||
if linked_order_has_payment_terms_template(po_or_so, doctype):
|
||||
return True
|
||||
elif linked_order_has_payment_schedule(po_or_so):
|
||||
return True
|
||||
return False
|
||||
|
||||
def all_items_have_same_po_or_so(self, po_or_so, fieldname) -> bool:
|
||||
for item in self.doc.get("items"):
|
||||
if item.get(fieldname) != po_or_so:
|
||||
return False
|
||||
return True
|
||||
|
||||
def fetch_payment_terms_from_order(
|
||||
self,
|
||||
po_or_so,
|
||||
po_or_so_doctype,
|
||||
grand_total,
|
||||
base_grand_total,
|
||||
automatically_fetch_payment_terms,
|
||||
) -> None:
|
||||
"""Fetch Payment Terms from Purchase/Sales Order when creating a new invoice."""
|
||||
doc = self.doc
|
||||
po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so)
|
||||
|
||||
doc.payment_schedule = []
|
||||
doc.payment_terms_template = po_or_so.payment_terms_template
|
||||
posting_date = doc.get("bill_date") or doc.get("posting_date") or doc.get("transaction_date")
|
||||
|
||||
for schedule in po_or_so.payment_schedule:
|
||||
payment_schedule = {
|
||||
"payment_term": schedule.payment_term,
|
||||
"due_date": schedule.due_date,
|
||||
"invoice_portion": schedule.invoice_portion,
|
||||
"mode_of_payment": schedule.mode_of_payment,
|
||||
"description": schedule.description,
|
||||
"paid_amount": schedule.paid_amount,
|
||||
}
|
||||
|
||||
if automatically_fetch_payment_terms:
|
||||
if schedule.due_date_based_on:
|
||||
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
|
||||
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
|
||||
payment_schedule["credit_days"] = cint(schedule.credit_days)
|
||||
payment_schedule["credit_months"] = cint(schedule.credit_months)
|
||||
|
||||
if schedule.discount_validity_based_on and flt(schedule.discount):
|
||||
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
|
||||
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
|
||||
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
|
||||
|
||||
payment_schedule["payment_amount"] = flt(
|
||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("payment_amount"),
|
||||
)
|
||||
payment_schedule["base_payment_amount"] = flt(
|
||||
base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("base_payment_amount"),
|
||||
)
|
||||
payment_schedule["outstanding"] = payment_schedule["payment_amount"]
|
||||
else:
|
||||
payment_schedule["base_payment_amount"] = flt(
|
||||
schedule.base_payment_amount * doc.get("conversion_rate"),
|
||||
schedule.precision("base_payment_amount"),
|
||||
)
|
||||
|
||||
if schedule.discount_type == "Percentage":
|
||||
payment_schedule["discount_type"] = schedule.discount_type
|
||||
payment_schedule["discount"] = schedule.discount
|
||||
|
||||
if not schedule.invoice_portion:
|
||||
payment_schedule["payment_amount"] = schedule.payment_amount
|
||||
|
||||
doc.append("payment_schedule", payment_schedule)
|
||||
|
||||
def set_due_date(self) -> None:
|
||||
due_dates = [d.due_date for d in self.doc.get("payment_schedule") if d.due_date]
|
||||
if due_dates:
|
||||
self.doc.due_date = max(due_dates)
|
||||
|
||||
def validate_payment_schedule_dates(self) -> None:
|
||||
dates = []
|
||||
li = []
|
||||
doc = self.doc
|
||||
|
||||
if doc.doctype == "Sales Invoice" and doc.is_pos:
|
||||
return
|
||||
|
||||
for d in doc.get("payment_schedule"):
|
||||
if not flt(d.discount):
|
||||
d.discount_date = None
|
||||
d.validate_from_to_dates("discount_date", "due_date")
|
||||
if doc.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
|
||||
doc.transaction_date
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
|
||||
d.idx
|
||||
)
|
||||
)
|
||||
elif d.due_date in dates:
|
||||
li.append(_("{0} in row {1}").format(d.due_date, d.idx))
|
||||
dates.append(d.due_date)
|
||||
|
||||
if li:
|
||||
frappe.throw(
|
||||
_("Rows with duplicate due dates in other rows were found: {0}").format(
|
||||
"<br>" + "<br>".join(li)
|
||||
),
|
||||
title=_("Payment Schedule"),
|
||||
)
|
||||
|
||||
def validate_payment_schedule_amount(self) -> None:
|
||||
doc = self.doc
|
||||
if (doc.doctype == "Sales Invoice" and doc.is_pos) or doc.get("is_opening") == "Yes":
|
||||
return
|
||||
|
||||
party_account_currency = doc.get("party_account_currency")
|
||||
if not party_account_currency:
|
||||
party_type, party = doc.get_party()
|
||||
if party_type and party:
|
||||
party_account_currency = get_party_account_currency(party_type, party, doc.company)
|
||||
|
||||
if doc.get("payment_schedule"):
|
||||
total = 0
|
||||
base_total = 0
|
||||
for d in doc.get("payment_schedule"):
|
||||
total += flt(d.payment_amount, d.precision("payment_amount"))
|
||||
base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
|
||||
|
||||
base_grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
||||
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
|
||||
|
||||
if doc.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
base_grand_total = base_grand_total - flt(doc.base_write_off_amount)
|
||||
grand_total = grand_total - flt(doc.write_off_amount)
|
||||
|
||||
if doc.get("total_advance"):
|
||||
if party_account_currency == doc.company_currency:
|
||||
base_grand_total -= doc.get("total_advance")
|
||||
grand_total = flt(
|
||||
base_grand_total / doc.get("conversion_rate"), doc.precision("grand_total")
|
||||
)
|
||||
else:
|
||||
grand_total -= doc.get("total_advance")
|
||||
base_grand_total = flt(
|
||||
grand_total * doc.get("conversion_rate"), doc.precision("base_grand_total")
|
||||
)
|
||||
|
||||
if (
|
||||
abs(flt(total, doc.precision("grand_total")) - flt(grand_total, doc.precision("grand_total")))
|
||||
> 0.1
|
||||
or abs(
|
||||
flt(base_total, doc.precision("base_grand_total"))
|
||||
- flt(base_grand_total, doc.precision("base_grand_total"))
|
||||
)
|
||||
> 0.1
|
||||
):
|
||||
frappe.throw(
|
||||
_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")
|
||||
)
|
||||
|
||||
|
||||
def linked_order_has_payment_terms_template(po_or_so, doctype) -> str | None:
|
||||
return frappe.get_value(doctype, po_or_so, "payment_terms_template")
|
||||
|
||||
|
||||
def linked_order_has_payment_schedule(po_or_so) -> list:
|
||||
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
|
||||
|
||||
|
||||
def get_payment_terms(
|
||||
terms_template: str,
|
||||
posting_date: DateTimeLikeObject | None = None,
|
||||
grand_total: float | None = None,
|
||||
base_grand_total: float | None = None,
|
||||
bill_date: DateTimeLikeObject | None = None,
|
||||
) -> list:
|
||||
if not terms_template:
|
||||
return
|
||||
|
||||
terms_doc = frappe.get_doc("Payment Terms Template", terms_template)
|
||||
schedule = []
|
||||
for d in terms_doc.get("terms"):
|
||||
d = frappe._dict(d.as_dict())
|
||||
term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date)
|
||||
schedule.append(term_details)
|
||||
|
||||
return schedule
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_term_details(
|
||||
term: str | frappe._dict,
|
||||
posting_date: DateTimeLikeObject | None = None,
|
||||
grand_total: float | None = None,
|
||||
base_grand_total: float | None = None,
|
||||
bill_date: DateTimeLikeObject | None = None,
|
||||
) -> frappe._dict:
|
||||
term_details = frappe._dict()
|
||||
if isinstance(term, str):
|
||||
term = frappe.get_doc("Payment Term", term)
|
||||
else:
|
||||
term_details.payment_term = term.payment_term
|
||||
|
||||
for field in [
|
||||
"description",
|
||||
"invoice_portion",
|
||||
"discount_type",
|
||||
"discount",
|
||||
"mode_of_payment",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
]:
|
||||
term_details[field] = term.get(field)
|
||||
|
||||
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
|
||||
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
|
||||
term_details.outstanding = term_details.payment_amount
|
||||
term_details.base_outstanding = term_details.base_payment_amount
|
||||
|
||||
has_discount = flt(term.get("discount"))
|
||||
date = bill_date or posting_date
|
||||
if date:
|
||||
term_details.due_date = get_due_date(term, date)
|
||||
term_details.discount_date = get_discount_date(term, date) if has_discount else None
|
||||
|
||||
if posting_date and getdate(term_details.due_date) < getdate(posting_date):
|
||||
term_details.due_date = posting_date
|
||||
|
||||
return term_details
|
||||
|
||||
|
||||
def get_due_date(term, posting_date=None, bill_date=None):
|
||||
due_date = None
|
||||
date = bill_date or posting_date
|
||||
if term.due_date_based_on == "Day(s) after invoice date":
|
||||
due_date = add_days(date, cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
|
||||
due_date = add_days(get_last_day(date), cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
|
||||
due_date = get_last_day(add_months(date, cint(term.credit_months)))
|
||||
return due_date
|
||||
|
||||
|
||||
def get_discount_date(term, posting_date=None, bill_date=None):
|
||||
discount_validity = None
|
||||
date = bill_date or posting_date
|
||||
if term.discount_validity_based_on == "Day(s) after invoice date":
|
||||
discount_validity = add_days(date, cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
|
||||
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
|
||||
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
|
||||
return discount_validity
|
||||
446
erpnext/accounts/services/taxes.py
Normal file
446
erpnext/accounts/services/taxes.py
Normal file
@@ -0,0 +1,446 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Tax helpers: TaxService class for doc-mutating operations, free functions for stateless utilities."""
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.utils import cint, flt, parse_json
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.get_item_details import (
|
||||
NOT_APPLICABLE_TAX,
|
||||
ItemDetailsCtx,
|
||||
_get_item_tax_template,
|
||||
_get_item_tax_template_from_item_group,
|
||||
get_item_tax_map,
|
||||
)
|
||||
|
||||
|
||||
class TaxService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def set_taxes(self) -> None:
|
||||
doc = self.doc
|
||||
if not doc.meta.get_field("taxes"):
|
||||
return
|
||||
|
||||
tax_master_doctype = doc.meta.get_field("taxes_and_charges").options
|
||||
|
||||
if (doc.is_new() or self.is_pos_profile_changed()) and not doc.get("taxes"):
|
||||
if doc.company and not doc.get("taxes_and_charges"):
|
||||
doc.taxes_and_charges = frappe.db.get_value(
|
||||
tax_master_doctype, {"is_default": 1, "company": doc.company}
|
||||
)
|
||||
self.append_taxes_from_master(tax_master_doctype)
|
||||
|
||||
def is_pos_profile_changed(self) -> bool:
|
||||
doc = self.doc
|
||||
if (
|
||||
doc.doctype == "Sales Invoice"
|
||||
and doc.is_pos
|
||||
and doc.pos_profile != frappe.db.get_value("Sales Invoice", doc.name, "pos_profile")
|
||||
):
|
||||
return True
|
||||
|
||||
def set_taxes_and_charges(self) -> None:
|
||||
doc = self.doc
|
||||
if doc.doctype == "Material Request":
|
||||
return
|
||||
|
||||
if doc.get("taxes") or doc.get("is_pos"):
|
||||
return
|
||||
|
||||
if frappe.get_single_value(
|
||||
"Accounts Settings", "add_taxes_from_taxes_and_charges_template"
|
||||
) and hasattr(doc, "taxes_and_charges"):
|
||||
if tax_master_doctype := doc.meta.get_field("taxes_and_charges").options:
|
||||
self.append_taxes_from_master(tax_master_doctype)
|
||||
|
||||
if frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
|
||||
self.append_taxes_from_item_tax_template()
|
||||
|
||||
def append_taxes_from_master(self, tax_master_doctype=None) -> None:
|
||||
doc = self.doc
|
||||
if doc.get("taxes_and_charges"):
|
||||
if not tax_master_doctype:
|
||||
tax_master_doctype = doc.meta.get_field("taxes_and_charges").options
|
||||
doc.extend("taxes", get_taxes_and_charges(tax_master_doctype, doc.get("taxes_and_charges")))
|
||||
|
||||
def append_taxes_from_item_tax_template(self) -> None:
|
||||
doc = self.doc
|
||||
if not frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
|
||||
return
|
||||
|
||||
for row in doc.items:
|
||||
item_tax_rate = row.get("item_tax_rate")
|
||||
if not item_tax_rate:
|
||||
continue
|
||||
|
||||
if isinstance(item_tax_rate, str):
|
||||
item_tax_rate = parse_json(item_tax_rate)
|
||||
|
||||
for account_head, _rate in item_tax_rate.items():
|
||||
if not self.get_tax_row(account_head):
|
||||
doc.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": account_head,
|
||||
"rate": 0,
|
||||
"description": account_head,
|
||||
"set_by_item_tax_template": 1,
|
||||
"category": "Total",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
def get_tax_row(self, account_head):
|
||||
for row in self.doc.taxes:
|
||||
if row.account_head == account_head:
|
||||
return row
|
||||
|
||||
def set_other_charges(self) -> None:
|
||||
self.doc.set("taxes", [])
|
||||
self.set_taxes()
|
||||
|
||||
def validate_enabled_taxes_and_charges(self) -> None:
|
||||
doc = self.doc
|
||||
taxes_and_charges_doctype = doc.meta.get_options("taxes_and_charges")
|
||||
if doc.taxes_and_charges and frappe.get_cached_value(
|
||||
taxes_and_charges_doctype, doc.taxes_and_charges, "disabled"
|
||||
):
|
||||
frappe.throw(_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, doc.taxes_and_charges))
|
||||
|
||||
def validate_tax_account_company(self) -> None:
|
||||
doc = self.doc
|
||||
for d in doc.get("taxes"):
|
||||
if d.account_head:
|
||||
tax_account_company = frappe.get_cached_value("Account", d.account_head, "company")
|
||||
if tax_account_company != doc.company:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Account {1} does not belong to company {2}").format(
|
||||
d.idx, d.account_head, doc.company
|
||||
)
|
||||
)
|
||||
|
||||
def get_tax_map(self) -> dict:
|
||||
tax_map = {}
|
||||
for tax in self.doc.get("taxes"):
|
||||
tax_map.setdefault(tax.account_head, 0.0)
|
||||
tax_map[tax.account_head] += tax.tax_amount
|
||||
return tax_map
|
||||
|
||||
def get_amount_and_base_amount(self, item, enable_discount_accounting):
|
||||
doc = self.doc
|
||||
amount = item.net_amount
|
||||
base_amount = item.base_net_amount
|
||||
|
||||
if (
|
||||
enable_discount_accounting
|
||||
and doc.get("discount_amount")
|
||||
and doc.get("additional_discount_account")
|
||||
):
|
||||
if not hasattr(doc, "__has_distributed_discount_set"):
|
||||
doc.__has_distributed_discount_set = any(
|
||||
i.distributed_discount_amount for i in doc.get("items")
|
||||
)
|
||||
|
||||
if not doc.__has_distributed_discount_set:
|
||||
return item.amount, item.base_amount
|
||||
|
||||
amount += item.distributed_discount_amount
|
||||
base_amount += flt(
|
||||
item.distributed_discount_amount * doc.get("conversion_rate"),
|
||||
item.precision("distributed_discount_amount"),
|
||||
)
|
||||
|
||||
return amount, base_amount
|
||||
|
||||
def get_tax_amounts(self, tax, enable_discount_accounting):
|
||||
doc = self.doc
|
||||
amount = tax.tax_amount_after_discount_amount
|
||||
base_amount = tax.base_tax_amount_after_discount_amount
|
||||
|
||||
if (
|
||||
enable_discount_accounting
|
||||
and doc.get("discount_amount")
|
||||
and doc.get("additional_discount_account")
|
||||
and doc.get("apply_discount_on") == "Grand Total"
|
||||
):
|
||||
amount = tax.tax_amount
|
||||
base_amount = tax.base_tax_amount
|
||||
|
||||
return amount, base_amount
|
||||
|
||||
|
||||
def get_tax_rate(account_head: str) -> dict:
|
||||
return frappe.get_cached_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_taxes_and_charges(
|
||||
master_doctype: str, tax_template: str | None = None, company: str | None = None
|
||||
) -> dict | None:
|
||||
if not company:
|
||||
return {}
|
||||
|
||||
if tax_template and company:
|
||||
tax_template_company = frappe.get_cached_value(master_doctype, tax_template, "company")
|
||||
if tax_template_company == company:
|
||||
return
|
||||
|
||||
default_tax = frappe.db.get_value(master_doctype, {"is_default": 1, "company": company})
|
||||
|
||||
return {
|
||||
"taxes_and_charges": default_tax,
|
||||
"taxes": get_taxes_and_charges(master_doctype, default_tax),
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_taxes_and_charges(master_doctype: str, master_name: str | None = None) -> list | None:
|
||||
if not master_name:
|
||||
return
|
||||
from frappe.model import child_table_fields, default_fields
|
||||
|
||||
tax_master = frappe.get_doc(master_doctype, master_name)
|
||||
|
||||
taxes_and_charges = []
|
||||
for _i, tax in enumerate(tax_master.get("taxes")):
|
||||
tax = tax.as_dict()
|
||||
|
||||
for fieldname in default_fields + child_table_fields:
|
||||
if fieldname in tax:
|
||||
del tax[fieldname]
|
||||
|
||||
taxes_and_charges.append(tax)
|
||||
|
||||
return taxes_and_charges
|
||||
|
||||
|
||||
def validate_conversion_rate(
|
||||
currency: str, conversion_rate: float, conversion_rate_label: str, company: str
|
||||
) -> None:
|
||||
"""Throw a validation error if conversion_rate is falsy."""
|
||||
company_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
|
||||
if not conversion_rate:
|
||||
throw(
|
||||
_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.").format(
|
||||
conversion_rate_label, currency, company_currency
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def validate_taxes_and_charges(tax) -> None:
|
||||
if tax.charge_type in ["Actual", "On Net Total", "On Paid Amount"] and tax.row_id:
|
||||
frappe.throw(
|
||||
_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'")
|
||||
)
|
||||
elif tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]:
|
||||
if cint(tax.idx) == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
|
||||
)
|
||||
)
|
||||
elif not tax.row_id:
|
||||
frappe.throw(
|
||||
_("Please specify a valid Row ID for row {0} in table {1}").format(tax.idx, _(tax.doctype))
|
||||
)
|
||||
elif tax.row_id and cint(tax.row_id) >= cint(tax.idx):
|
||||
frappe.throw(
|
||||
_("Cannot refer row number greater than or equal to current row number for this Charge type")
|
||||
)
|
||||
|
||||
if tax.charge_type == "Actual":
|
||||
tax.rate = None
|
||||
|
||||
|
||||
def validate_account_head(idx: int, account: str, company: str, context: str | None = None) -> None:
|
||||
"""Throw a ValidationError if the account belongs to a different company or is a group account."""
|
||||
if company != frappe.get_cached_value("Account", account, "company"):
|
||||
frappe.throw(
|
||||
_("Row {0}: The {3} Account {1} does not belong to the company {2}").format(
|
||||
idx, frappe.bold(account), frappe.bold(company), context or ""
|
||||
),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
if frappe.get_cached_value("Account", account, "is_group"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"You selected the account group {1} as {2} Account in row {0}. Please select a single account."
|
||||
).format(idx, frappe.bold(account), context or ""),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
|
||||
def validate_cost_center(tax, doc) -> None:
|
||||
if not tax.cost_center:
|
||||
return
|
||||
|
||||
company = frappe.get_cached_value("Cost Center", tax.cost_center, "company")
|
||||
|
||||
if company != doc.company:
|
||||
frappe.throw(
|
||||
_("Row {0}: Cost Center {1} does not belong to Company {2}").format(
|
||||
tax.idx, frappe.bold(tax.cost_center), frappe.bold(doc.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
|
||||
|
||||
def validate_inclusive_tax(tax, doc) -> None:
|
||||
def _on_previous_row_error(row_range):
|
||||
throw(
|
||||
_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(
|
||||
tax.idx, row_range
|
||||
)
|
||||
)
|
||||
|
||||
if cint(getattr(tax, "included_in_print_rate", None)):
|
||||
if tax.charge_type == "Actual":
|
||||
throw(
|
||||
_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(
|
||||
tax.idx
|
||||
)
|
||||
)
|
||||
elif tax.charge_type == "On Previous Row Amount" and not cint(
|
||||
doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate
|
||||
):
|
||||
_on_previous_row_error(tax.row_id)
|
||||
elif tax.charge_type == "On Previous Row Total" and not all(
|
||||
[cint(t.included_in_print_rate) for t in doc.get("taxes")[: cint(tax.row_id) - 1]]
|
||||
):
|
||||
_on_previous_row_error("1 - %d" % (tax.row_id,))
|
||||
elif tax.get("category") == "Valuation":
|
||||
frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
|
||||
|
||||
|
||||
def set_balance_in_account_currency(
|
||||
gl_dict,
|
||||
account_currency: str | None = None,
|
||||
conversion_rate: float | None = None,
|
||||
company_currency: str | None = None,
|
||||
) -> None:
|
||||
if (not conversion_rate) and (account_currency != company_currency):
|
||||
frappe.throw(
|
||||
_("Account: {0} with currency: {1} can not be selected").format(gl_dict.account, account_currency)
|
||||
)
|
||||
|
||||
gl_dict["account_currency"] = account_currency
|
||||
|
||||
if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency):
|
||||
gl_dict.debit_in_account_currency = (
|
||||
gl_dict.debit if account_currency == company_currency else flt(gl_dict.debit / conversion_rate, 2)
|
||||
)
|
||||
|
||||
if flt(gl_dict.credit) and not flt(gl_dict.credit_in_account_currency):
|
||||
gl_dict.credit_in_account_currency = (
|
||||
gl_dict.credit
|
||||
if account_currency == company_currency
|
||||
else flt(gl_dict.credit / conversion_rate, 2)
|
||||
)
|
||||
|
||||
|
||||
def set_child_tax_template_and_map(item, child_item, parent_doc) -> None:
|
||||
ctx = ItemDetailsCtx(
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"posting_date": parent_doc.transaction_date,
|
||||
"tax_category": parent_doc.get("tax_category"),
|
||||
"company": parent_doc.get("company"),
|
||||
"base_net_rate": item.get("base_net_rate"),
|
||||
}
|
||||
)
|
||||
|
||||
item_tax_template = _get_item_tax_template(ctx, item.taxes)
|
||||
|
||||
if not item_tax_template:
|
||||
item_tax_template = _get_item_tax_template_from_item_group(ctx, item.item_group)
|
||||
|
||||
child_item.item_tax_template = item_tax_template
|
||||
child_item.item_tax_rate = get_item_tax_map(
|
||||
doc=parent_doc,
|
||||
tax_template=child_item.item_tax_template,
|
||||
as_json=True,
|
||||
)
|
||||
|
||||
|
||||
def add_taxes_from_tax_template(child_item, parent_doc, db_insert: bool = True) -> None:
|
||||
add_taxes_from_item_tax_template = frappe.get_single_value(
|
||||
"Accounts Settings", "add_taxes_from_item_tax_template"
|
||||
)
|
||||
|
||||
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
|
||||
tax_map = json.loads(child_item.get("item_tax_rate"))
|
||||
for tax_type, tax_rate in tax_map.items():
|
||||
if tax_rate == NOT_APPLICABLE_TAX:
|
||||
continue
|
||||
|
||||
tax_rate = flt(tax_rate)
|
||||
taxes = parent_doc.get("taxes") or []
|
||||
found = any(tax.account_head == tax_type for tax in taxes)
|
||||
if not found:
|
||||
tax_row = parent_doc.append("taxes", {})
|
||||
tax_row.update(
|
||||
{
|
||||
"description": str(tax_type).split(" - ")[0],
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": tax_type,
|
||||
"rate": tax_rate,
|
||||
"set_by_item_tax_template": 1,
|
||||
}
|
||||
)
|
||||
if parent_doc.doctype == "Purchase Order":
|
||||
tax_row.update({"category": "Total", "add_deduct_tax": "Add"})
|
||||
if db_insert:
|
||||
tax_row.db_insert()
|
||||
|
||||
|
||||
def merge_taxes(source_doc, target_doc) -> None:
|
||||
tax_map = {}
|
||||
for tax in source_doc.get("taxes") or []:
|
||||
found = False
|
||||
for t in target_doc.get("taxes") or []:
|
||||
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
||||
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
||||
tax_map[tax.name] = t
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
tax.charge_type = "Actual"
|
||||
tax.included_in_print_rate = 0
|
||||
tax.dont_recompute_tax = 1
|
||||
tax.row_id = None
|
||||
tax.idx = None
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
||||
tax_map[tax.name] = target_doc.append("taxes", tax)
|
||||
|
||||
item_map = {d._old_name: d for d in target_doc.get("items") if d.get("_old_name")}
|
||||
|
||||
item_tax_details = target_doc.get("_item_wise_tax_details") or []
|
||||
for row in source_doc.get("item_wise_tax_details"):
|
||||
item = item_map.get(row.item_row)
|
||||
tax = tax_map.get(row.tax_row)
|
||||
if not (item and tax):
|
||||
continue
|
||||
|
||||
item_tax_details.append(
|
||||
frappe._dict(
|
||||
item=item,
|
||||
tax=tax,
|
||||
amount=row.amount,
|
||||
rate=row.rate,
|
||||
taxable_amount=row.taxable_amount,
|
||||
)
|
||||
)
|
||||
|
||||
target_doc._item_wise_tax_details = item_tax_details
|
||||
@@ -46,7 +46,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.make_methods = {
|
||||
"Asset Movement": () => {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
|
||||
method: "erpnext.assets.doctype.asset.mapper.make_asset_movement",
|
||||
freeze: true,
|
||||
args: {
|
||||
assets: [{ name: frm.doc.name }],
|
||||
@@ -333,7 +333,7 @@ frappe.ui.form.on("Asset", {
|
||||
|
||||
make_journal_entry: function (frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_journal_entry",
|
||||
method: "erpnext.assets.doctype.asset.mapper.make_journal_entry",
|
||||
args: {
|
||||
asset_name: frm.doc.name,
|
||||
},
|
||||
@@ -570,7 +570,7 @@ frappe.ui.form.on("Asset", {
|
||||
asset_category: frm.doc.asset_category,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.create_asset_maintenance",
|
||||
method: "erpnext.assets.doctype.asset.mapper.create_asset_maintenance",
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
@@ -585,7 +585,7 @@ frappe.ui.form.on("Asset", {
|
||||
asset: frm.doc.name,
|
||||
asset_name: frm.doc.asset_name,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.create_asset_repair",
|
||||
method: "erpnext.assets.doctype.asset.mapper.create_asset_repair",
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
@@ -601,7 +601,7 @@ frappe.ui.form.on("Asset", {
|
||||
asset_name: frm.doc.asset_name,
|
||||
item_code: frm.doc.item_code,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization",
|
||||
method: "erpnext.assets.doctype.asset.mapper.create_asset_capitalization",
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
@@ -612,7 +612,7 @@ frappe.ui.form.on("Asset", {
|
||||
sell_asset: function (frm) {
|
||||
const make_sales_invoice = (sell_qty) => {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_sales_invoice",
|
||||
method: "erpnext.assets.doctype.asset.mapper.make_sales_invoice",
|
||||
args: {
|
||||
asset: frm.doc.name,
|
||||
item_code: frm.doc.item_code,
|
||||
@@ -696,7 +696,7 @@ frappe.ui.form.on("Asset", {
|
||||
asset_name: frm.doc.name,
|
||||
split_qty: cint(dialog_data.split_qty),
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.split_asset",
|
||||
method: "erpnext.assets.doctype.asset.mapper.split_asset",
|
||||
callback: function (r) {
|
||||
let doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
@@ -716,7 +716,7 @@ frappe.ui.form.on("Asset", {
|
||||
asset_category: frm.doc.asset_category,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment",
|
||||
method: "erpnext.assets.doctype.asset.mapper.create_asset_value_adjustment",
|
||||
freeze: 1,
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
@@ -967,7 +967,7 @@ erpnext.asset.restore_asset = function (frm) {
|
||||
|
||||
erpnext.asset.transfer_asset = function (frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
|
||||
method: "erpnext.assets.doctype.asset.mapper.make_asset_movement",
|
||||
freeze: true,
|
||||
args: {
|
||||
assets: [{ name: frm.doc.name }],
|
||||
|
||||
@@ -21,7 +21,6 @@ from frappe.utils import (
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_comma_separated_links,
|
||||
@@ -1092,101 +1091,6 @@ def get_asset_naming_series():
|
||||
return meta.get_field("naming_series").options
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(asset: str, item_code: str, company: str, sell_qty: int, serial_no: str | None = None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
si.company = company
|
||||
si.currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(company)
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"is_fixed_asset": 1,
|
||||
"asset": asset,
|
||||
"income_account": disposal_account,
|
||||
"serial_no": serial_no,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"qty": sell_qty,
|
||||
},
|
||||
)
|
||||
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
si.update(
|
||||
{
|
||||
dimension["fieldname"]: asset_doc.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
si.set_missing_values()
|
||||
return si
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_maintenance(
|
||||
asset: str,
|
||||
item_code: str,
|
||||
item_name: str,
|
||||
asset_category: str,
|
||||
company: str,
|
||||
):
|
||||
asset_maintenance = frappe.new_doc("Asset Maintenance")
|
||||
asset_maintenance.update(
|
||||
{
|
||||
"asset_name": asset,
|
||||
"company": company,
|
||||
"item_code": item_code,
|
||||
"item_name": item_name,
|
||||
"asset_category": asset_category,
|
||||
}
|
||||
)
|
||||
return asset_maintenance
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_repair(
|
||||
company: str,
|
||||
asset: str,
|
||||
asset_name: str,
|
||||
):
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
|
||||
return asset_repair
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_capitalization(
|
||||
company: str,
|
||||
asset: str,
|
||||
asset_name: str,
|
||||
item_code: str,
|
||||
):
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
asset_capitalization.update(
|
||||
{
|
||||
"target_asset": asset,
|
||||
"company": company,
|
||||
"target_asset_name": asset_name,
|
||||
"target_item_code": item_code,
|
||||
}
|
||||
)
|
||||
return asset_capitalization
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_value_adjustment(
|
||||
asset: str,
|
||||
asset_category: str,
|
||||
company: str,
|
||||
):
|
||||
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
||||
asset_value_adjustment.update({"asset": asset, "company": company, "asset_category": asset_category})
|
||||
return asset_value_adjustment
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(
|
||||
item_code: str,
|
||||
@@ -1241,77 +1145,6 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
|
||||
return account
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_journal_entry(asset_name: str):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.company = asset.company
|
||||
je.remark = _("Depreciation Entry against asset {0}").format(asset_name)
|
||||
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": depreciation_expense_account,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
},
|
||||
)
|
||||
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": accumulated_depreciation_account,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
},
|
||||
)
|
||||
|
||||
return je
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_asset_movement(
|
||||
assets: list[dict] | str,
|
||||
purpose: str = "Transfer",
|
||||
):
|
||||
if isinstance(assets, str):
|
||||
assets = json.loads(assets)
|
||||
|
||||
if len(assets) == 0:
|
||||
frappe.throw(_("At least one asset has to be selected."))
|
||||
|
||||
asset_movement = frappe.new_doc("Asset Movement")
|
||||
asset_movement.purpose = purpose
|
||||
for asset in assets:
|
||||
asset = frappe.get_doc("Asset", asset.get("name"))
|
||||
asset_movement.company = asset.get("company")
|
||||
asset_movement.append(
|
||||
"assets",
|
||||
{
|
||||
"asset": asset.get("name"),
|
||||
"source_location": asset.get("location"),
|
||||
"from_employee": asset.get("custodian"),
|
||||
},
|
||||
)
|
||||
|
||||
if asset_movement.get("assets"):
|
||||
return asset_movement.as_dict()
|
||||
|
||||
|
||||
def is_cwip_accounting_enabled(asset_category):
|
||||
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting"))
|
||||
|
||||
@@ -1360,216 +1193,3 @@ def get_values_from_purchase_doc(
|
||||
"purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_asset(asset_name: str, split_qty: int):
|
||||
"""Split an asset into two based on the given quantity."""
|
||||
existing_asset = frappe.get_doc("Asset", asset_name)
|
||||
split_qty = cint(split_qty)
|
||||
|
||||
validate_split_quantity(existing_asset, split_qty)
|
||||
remaining_qty = existing_asset.asset_quantity - split_qty
|
||||
|
||||
# Create new asset and update existing one
|
||||
splitted_asset = create_new_asset_from_split(existing_asset, split_qty)
|
||||
update_existing_asset_after_split(existing_asset, remaining_qty, splitted_asset)
|
||||
|
||||
return splitted_asset
|
||||
|
||||
|
||||
def validate_split_quantity(existing_asset, split_qty):
|
||||
if split_qty >= existing_asset.asset_quantity:
|
||||
frappe.throw(_("Split Quantity must be less than Asset Quantity"))
|
||||
|
||||
|
||||
def create_new_asset_from_split(existing_asset, split_qty):
|
||||
"""Create a new asset from the split quantity."""
|
||||
return process_asset_split(existing_asset, split_qty, is_new_asset=True)
|
||||
|
||||
|
||||
def update_existing_asset_after_split(existing_asset, remaining_qty, splitted_asset):
|
||||
"""Update the existing asset with the remaining quantity."""
|
||||
process_asset_split(existing_asset, remaining_qty, splitted_asset=splitted_asset)
|
||||
|
||||
|
||||
def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_asset=False):
|
||||
"""Handle asset creation or update during the split."""
|
||||
scaling_factor = flt(split_qty) / flt(existing_asset.asset_quantity)
|
||||
new_asset = frappe.copy_doc(existing_asset) if is_new_asset else splitted_asset
|
||||
asset_doc = new_asset if is_new_asset else existing_asset
|
||||
asset_doc.flags.is_split_asset = True
|
||||
|
||||
set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset)
|
||||
log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset)
|
||||
|
||||
# Update finance books and depreciation schedules
|
||||
update_finance_books(asset_doc, existing_asset, new_asset, scaling_factor, is_new_asset)
|
||||
return new_asset
|
||||
|
||||
|
||||
def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
|
||||
asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
||||
asset_doc.purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
||||
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
|
||||
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
|
||||
asset_doc.opening_accumulated_depreciation = (
|
||||
existing_asset.opening_accumulated_depreciation * scaling_factor
|
||||
)
|
||||
asset_doc.value_after_depreciation = existing_asset.value_after_depreciation * scaling_factor
|
||||
asset_doc.asset_quantity = split_qty
|
||||
asset_doc.split_from = existing_asset.name if is_new_asset else None
|
||||
|
||||
for row in asset_doc.get("finance_books"):
|
||||
row.value_after_depreciation = row.value_after_depreciation * scaling_factor
|
||||
row.expected_value_after_useful_life = row.expected_value_after_useful_life * scaling_factor
|
||||
|
||||
if not is_new_asset:
|
||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
asset_doc.save()
|
||||
|
||||
|
||||
def log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset):
|
||||
if is_new_asset:
|
||||
asset_doc.insert()
|
||||
add_asset_activity(
|
||||
asset_doc.name,
|
||||
_("Asset created after being split from Asset {0}").format(
|
||||
get_link_to_form("Asset", existing_asset.name)
|
||||
),
|
||||
)
|
||||
asset_doc.submit()
|
||||
asset_doc.set_status()
|
||||
else:
|
||||
add_asset_activity(
|
||||
existing_asset.name,
|
||||
_("Asset updated after being split into Asset {0}").format(
|
||||
get_link_to_form("Asset", splitted_asset.name)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def update_finance_books(asset_doc, existing_asset, new_asset, scaling_factor, is_new_asset):
|
||||
"""Update finance books and depreciation schedules for the asset."""
|
||||
for fb_row in asset_doc.get("finance_books"):
|
||||
reschedule_depr_for_updated_asset(existing_asset, new_asset, fb_row, scaling_factor, is_new_asset)
|
||||
|
||||
# Add references in journal entries for new asset
|
||||
if is_new_asset:
|
||||
for row in new_asset.get("finance_books"):
|
||||
depr_schedule_doc = get_depr_schedule(new_asset.name, "Active", row.finance_book)
|
||||
for schedule in depr_schedule_doc:
|
||||
if schedule.journal_entry:
|
||||
add_reference_in_jv_on_split(
|
||||
schedule.journal_entry,
|
||||
new_asset.name,
|
||||
existing_asset.name,
|
||||
schedule.depreciation_amount,
|
||||
)
|
||||
|
||||
|
||||
def reschedule_depr_for_updated_asset(existing_asset, new_asset, fb_row, scaling_factor, is_new_asset):
|
||||
"""Reschedule depreciation for an asset after a split."""
|
||||
current_depr_schedule_doc = get_asset_depr_schedule_doc(
|
||||
existing_asset.name, "Active", fb_row.finance_book
|
||||
)
|
||||
if not current_depr_schedule_doc:
|
||||
return
|
||||
|
||||
# Create a new depreciation schedule based on the current one
|
||||
new_depr_schedule_doc = create_new_depr_schedule(
|
||||
current_depr_schedule_doc, existing_asset, new_asset, is_new_asset, fb_row
|
||||
)
|
||||
|
||||
update_depreciation_terms(new_depr_schedule_doc, scaling_factor)
|
||||
add_depr_schedule_notes(new_depr_schedule_doc, existing_asset, new_asset, is_new_asset)
|
||||
|
||||
if not is_new_asset:
|
||||
current_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
|
||||
current_depr_schedule_doc.cancel()
|
||||
|
||||
new_depr_schedule_doc.submit()
|
||||
|
||||
|
||||
def create_new_depr_schedule(current_depr_schedule_doc, existing_asset, new_asset, is_new_asset, fb_row):
|
||||
"""Create a new depreciation schedule based on the current one."""
|
||||
new_depr_schedule_doc = frappe.copy_doc(current_depr_schedule_doc)
|
||||
new_depr_schedule_doc.asset_doc = new_asset if is_new_asset else existing_asset
|
||||
new_depr_schedule_doc.fb_row = fb_row
|
||||
new_depr_schedule_doc.fetch_asset_details()
|
||||
return new_depr_schedule_doc
|
||||
|
||||
|
||||
def update_depreciation_terms(new_depr_schedule_doc, scaling_factor):
|
||||
"""Update depreciation terms with scaled amounts."""
|
||||
accumulated_depreciation = 0
|
||||
for term in new_depr_schedule_doc.get("depreciation_schedule"):
|
||||
depreciation_amount = flt(
|
||||
term.depreciation_amount * scaling_factor, term.precision("depreciation_amount")
|
||||
)
|
||||
term.depreciation_amount = depreciation_amount
|
||||
accumulated_depreciation = flt(
|
||||
accumulated_depreciation + depreciation_amount, term.precision("depreciation_amount")
|
||||
)
|
||||
term.accumulated_depreciation_amount = accumulated_depreciation
|
||||
|
||||
|
||||
def add_depr_schedule_notes(new_depr_schedule_doc, existing_asset, new_asset, is_new_asset):
|
||||
notes = _("This schedule was created when Asset {0} was {1} into new Asset {2}.").format(
|
||||
get_link_to_form(existing_asset.doctype, existing_asset.name),
|
||||
"split" if is_new_asset else "updated after being split",
|
||||
get_link_to_form(new_asset.doctype, new_asset.name),
|
||||
)
|
||||
new_depr_schedule_doc.notes = notes
|
||||
|
||||
|
||||
def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount):
|
||||
"""Add a reference to a new asset in a journal entry after a split."""
|
||||
journal_entry = frappe.get_doc("Journal Entry", entry_name)
|
||||
entries_to_add = []
|
||||
|
||||
adjust_existing_accounts(journal_entry, old_asset_name, depreciation_amount, entries_to_add)
|
||||
add_new_entries(journal_entry, entries_to_add, new_asset_name, depreciation_amount)
|
||||
|
||||
# Save and repost the journal entry
|
||||
journal_entry.flags.ignore_validate_update_after_submit = True
|
||||
journal_entry.save()
|
||||
|
||||
journal_entry.docstatus = 2
|
||||
journal_entry.make_gl_entries(1)
|
||||
journal_entry.docstatus = 1
|
||||
journal_entry.make_gl_entries()
|
||||
|
||||
|
||||
def adjust_existing_accounts(journal_entry, old_asset_name, depreciation_amount, entries_to_add):
|
||||
"""Adjust existing accounts and prepare new entries for the new asset."""
|
||||
for account in journal_entry.get("accounts"):
|
||||
if account.reference_name == old_asset_name:
|
||||
entries_to_add.append(frappe.copy_doc(account).as_dict())
|
||||
adjust_account_balance(account, depreciation_amount)
|
||||
|
||||
|
||||
def adjust_account_balance(account, depreciation_amount):
|
||||
"""Adjust the balance of an account based on the depreciation amount."""
|
||||
if account.credit:
|
||||
account.credit -= depreciation_amount
|
||||
account.credit_in_account_currency -= account.exchange_rate * depreciation_amount
|
||||
elif account.debit:
|
||||
account.debit -= depreciation_amount
|
||||
account.debit_in_account_currency -= account.exchange_rate * depreciation_amount
|
||||
|
||||
|
||||
def add_new_entries(journal_entry, entries_to_add, new_asset_name, depreciation_amount):
|
||||
"""Add new entries for the new asset to the journal entry."""
|
||||
idx = len(journal_entry.get("accounts")) + 1
|
||||
for entry in entries_to_add:
|
||||
entry.reference_name = new_asset_name
|
||||
if entry.credit:
|
||||
entry.credit = depreciation_amount
|
||||
entry.credit_in_account_currency = entry.exchange_rate * depreciation_amount
|
||||
elif entry.debit:
|
||||
entry.debit = depreciation_amount
|
||||
entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount
|
||||
entry.idx = idx
|
||||
idx += 1
|
||||
journal_entry.append("accounts", entry)
|
||||
|
||||
@@ -32,7 +32,7 @@ frappe.listview_settings["Asset"] = {
|
||||
me.page.add_action_item(__("Make Asset Movement"), function () {
|
||||
const assets = me.get_checked_items();
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
|
||||
method: "erpnext.assets.doctype.asset.mapper.make_asset_movement",
|
||||
freeze: true,
|
||||
args: {
|
||||
assets: assets,
|
||||
|
||||
394
erpnext/assets/doctype/asset/mapper.py
Normal file
394
erpnext/assets/doctype/asset/mapper.py
Normal file
@@ -0,0 +1,394 @@
|
||||
# Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_depreciation_accounts,
|
||||
get_disposal_account_and_cost_center,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
get_depr_schedule,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(asset: str, item_code: str, company: str, sell_qty: int, serial_no: str | None = None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
si.company = company
|
||||
si.currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(company)
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"is_fixed_asset": 1,
|
||||
"asset": asset,
|
||||
"income_account": disposal_account,
|
||||
"serial_no": serial_no,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"qty": sell_qty,
|
||||
},
|
||||
)
|
||||
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
si.update(
|
||||
{
|
||||
dimension["fieldname"]: asset_doc.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
si.set_missing_values()
|
||||
return si
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_maintenance(
|
||||
asset: str,
|
||||
item_code: str,
|
||||
item_name: str,
|
||||
asset_category: str,
|
||||
company: str,
|
||||
):
|
||||
asset_maintenance = frappe.new_doc("Asset Maintenance")
|
||||
asset_maintenance.update(
|
||||
{
|
||||
"asset_name": asset,
|
||||
"company": company,
|
||||
"item_code": item_code,
|
||||
"item_name": item_name,
|
||||
"asset_category": asset_category,
|
||||
}
|
||||
)
|
||||
return asset_maintenance
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_repair(
|
||||
company: str,
|
||||
asset: str,
|
||||
asset_name: str,
|
||||
):
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
|
||||
return asset_repair
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_capitalization(
|
||||
company: str,
|
||||
asset: str,
|
||||
asset_name: str,
|
||||
item_code: str,
|
||||
):
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
asset_capitalization.update(
|
||||
{
|
||||
"target_asset": asset,
|
||||
"company": company,
|
||||
"target_asset_name": asset_name,
|
||||
"target_item_code": item_code,
|
||||
}
|
||||
)
|
||||
return asset_capitalization
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_value_adjustment(
|
||||
asset: str,
|
||||
asset_category: str,
|
||||
company: str,
|
||||
):
|
||||
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
||||
asset_value_adjustment.update({"asset": asset, "company": company, "asset_category": asset_category})
|
||||
return asset_value_adjustment
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_journal_entry(asset_name: str):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.company = asset.company
|
||||
je.remark = _("Depreciation Entry against asset {0}").format(asset_name)
|
||||
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": depreciation_expense_account,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
},
|
||||
)
|
||||
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": accumulated_depreciation_account,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
},
|
||||
)
|
||||
|
||||
return je
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_asset_movement(
|
||||
assets: list[dict] | str,
|
||||
purpose: str = "Transfer",
|
||||
):
|
||||
if isinstance(assets, str):
|
||||
assets = json.loads(assets)
|
||||
|
||||
if len(assets) == 0:
|
||||
frappe.throw(_("At least one asset has to be selected."))
|
||||
|
||||
asset_movement = frappe.new_doc("Asset Movement")
|
||||
asset_movement.purpose = purpose
|
||||
for asset in assets:
|
||||
asset = frappe.get_doc("Asset", asset.get("name"))
|
||||
asset_movement.company = asset.get("company")
|
||||
asset_movement.append(
|
||||
"assets",
|
||||
{
|
||||
"asset": asset.get("name"),
|
||||
"source_location": asset.get("location"),
|
||||
"from_employee": asset.get("custodian"),
|
||||
},
|
||||
)
|
||||
|
||||
if asset_movement.get("assets"):
|
||||
return asset_movement.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_asset(asset_name: str, split_qty: int):
|
||||
"""Split an asset into two based on the given quantity."""
|
||||
existing_asset = frappe.get_doc("Asset", asset_name)
|
||||
split_qty = cint(split_qty)
|
||||
|
||||
validate_split_quantity(existing_asset, split_qty)
|
||||
remaining_qty = existing_asset.asset_quantity - split_qty
|
||||
|
||||
splitted_asset = create_new_asset_from_split(existing_asset, split_qty)
|
||||
update_existing_asset_after_split(existing_asset, remaining_qty, splitted_asset)
|
||||
|
||||
return splitted_asset
|
||||
|
||||
|
||||
def validate_split_quantity(existing_asset, split_qty):
|
||||
if split_qty >= existing_asset.asset_quantity:
|
||||
frappe.throw(_("Split Quantity must be less than Asset Quantity"))
|
||||
|
||||
|
||||
def create_new_asset_from_split(existing_asset, split_qty):
|
||||
"""Create a new asset from the split quantity."""
|
||||
return process_asset_split(existing_asset, split_qty, is_new_asset=True)
|
||||
|
||||
|
||||
def update_existing_asset_after_split(existing_asset, remaining_qty, splitted_asset):
|
||||
"""Update the existing asset with the remaining quantity."""
|
||||
process_asset_split(existing_asset, remaining_qty, splitted_asset=splitted_asset)
|
||||
|
||||
|
||||
def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_asset=False):
|
||||
"""Handle asset creation or update during the split."""
|
||||
scaling_factor = flt(split_qty) / flt(existing_asset.asset_quantity)
|
||||
new_asset = frappe.copy_doc(existing_asset) if is_new_asset else splitted_asset
|
||||
asset_doc = new_asset if is_new_asset else existing_asset
|
||||
asset_doc.flags.is_split_asset = True
|
||||
|
||||
set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset)
|
||||
log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset)
|
||||
|
||||
update_finance_books(asset_doc, existing_asset, new_asset, scaling_factor, is_new_asset)
|
||||
return new_asset
|
||||
|
||||
|
||||
def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
|
||||
asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
||||
asset_doc.purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
||||
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
|
||||
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
|
||||
asset_doc.opening_accumulated_depreciation = (
|
||||
existing_asset.opening_accumulated_depreciation * scaling_factor
|
||||
)
|
||||
asset_doc.value_after_depreciation = existing_asset.value_after_depreciation * scaling_factor
|
||||
asset_doc.asset_quantity = split_qty
|
||||
asset_doc.split_from = existing_asset.name if is_new_asset else None
|
||||
|
||||
for row in asset_doc.get("finance_books"):
|
||||
row.value_after_depreciation = row.value_after_depreciation * scaling_factor
|
||||
row.expected_value_after_useful_life = row.expected_value_after_useful_life * scaling_factor
|
||||
|
||||
if not is_new_asset:
|
||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
asset_doc.save()
|
||||
|
||||
|
||||
def log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset):
|
||||
if is_new_asset:
|
||||
asset_doc.insert()
|
||||
add_asset_activity(
|
||||
asset_doc.name,
|
||||
_("Asset created after being split from Asset {0}").format(
|
||||
get_link_to_form("Asset", existing_asset.name)
|
||||
),
|
||||
)
|
||||
asset_doc.submit()
|
||||
asset_doc.set_status()
|
||||
else:
|
||||
add_asset_activity(
|
||||
existing_asset.name,
|
||||
_("Asset updated after being split into Asset {0}").format(
|
||||
get_link_to_form("Asset", splitted_asset.name)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def update_finance_books(asset_doc, existing_asset, new_asset, scaling_factor, is_new_asset):
|
||||
"""Update finance books and depreciation schedules for the asset."""
|
||||
for fb_row in asset_doc.get("finance_books"):
|
||||
reschedule_depr_for_updated_asset(existing_asset, new_asset, fb_row, scaling_factor, is_new_asset)
|
||||
|
||||
if is_new_asset:
|
||||
for row in new_asset.get("finance_books"):
|
||||
depr_schedule_doc = get_depr_schedule(new_asset.name, "Active", row.finance_book)
|
||||
for schedule in depr_schedule_doc:
|
||||
if schedule.journal_entry:
|
||||
add_reference_in_jv_on_split(
|
||||
schedule.journal_entry,
|
||||
new_asset.name,
|
||||
existing_asset.name,
|
||||
schedule.depreciation_amount,
|
||||
)
|
||||
|
||||
|
||||
def reschedule_depr_for_updated_asset(existing_asset, new_asset, fb_row, scaling_factor, is_new_asset):
|
||||
"""Reschedule depreciation for an asset after a split."""
|
||||
current_depr_schedule_doc = get_asset_depr_schedule_doc(
|
||||
existing_asset.name, "Active", fb_row.finance_book
|
||||
)
|
||||
if not current_depr_schedule_doc:
|
||||
return
|
||||
|
||||
new_depr_schedule_doc = create_new_depr_schedule(
|
||||
current_depr_schedule_doc, existing_asset, new_asset, is_new_asset, fb_row
|
||||
)
|
||||
|
||||
update_depreciation_terms(new_depr_schedule_doc, scaling_factor)
|
||||
add_depr_schedule_notes(new_depr_schedule_doc, existing_asset, new_asset, is_new_asset)
|
||||
|
||||
if not is_new_asset:
|
||||
current_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
|
||||
current_depr_schedule_doc.cancel()
|
||||
|
||||
new_depr_schedule_doc.submit()
|
||||
|
||||
|
||||
def create_new_depr_schedule(current_depr_schedule_doc, existing_asset, new_asset, is_new_asset, fb_row):
|
||||
"""Create a new depreciation schedule based on the current one."""
|
||||
new_depr_schedule_doc = frappe.copy_doc(current_depr_schedule_doc)
|
||||
new_depr_schedule_doc.asset_doc = new_asset if is_new_asset else existing_asset
|
||||
new_depr_schedule_doc.fb_row = fb_row
|
||||
new_depr_schedule_doc.fetch_asset_details()
|
||||
return new_depr_schedule_doc
|
||||
|
||||
|
||||
def update_depreciation_terms(new_depr_schedule_doc, scaling_factor):
|
||||
"""Update depreciation terms with scaled amounts."""
|
||||
accumulated_depreciation = 0
|
||||
for term in new_depr_schedule_doc.get("depreciation_schedule"):
|
||||
depreciation_amount = flt(
|
||||
term.depreciation_amount * scaling_factor, term.precision("depreciation_amount")
|
||||
)
|
||||
term.depreciation_amount = depreciation_amount
|
||||
accumulated_depreciation = flt(
|
||||
accumulated_depreciation + depreciation_amount, term.precision("depreciation_amount")
|
||||
)
|
||||
term.accumulated_depreciation_amount = accumulated_depreciation
|
||||
|
||||
|
||||
def add_depr_schedule_notes(new_depr_schedule_doc, existing_asset, new_asset, is_new_asset):
|
||||
notes = _("This schedule was created when Asset {0} was {1} into new Asset {2}.").format(
|
||||
get_link_to_form(existing_asset.doctype, existing_asset.name),
|
||||
"split" if is_new_asset else "updated after being split",
|
||||
get_link_to_form(new_asset.doctype, new_asset.name),
|
||||
)
|
||||
new_depr_schedule_doc.notes = notes
|
||||
|
||||
|
||||
def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount):
|
||||
"""Add a reference to a new asset in a journal entry after a split."""
|
||||
journal_entry = frappe.get_doc("Journal Entry", entry_name)
|
||||
entries_to_add = []
|
||||
|
||||
adjust_existing_accounts(journal_entry, old_asset_name, depreciation_amount, entries_to_add)
|
||||
add_new_entries(journal_entry, entries_to_add, new_asset_name, depreciation_amount)
|
||||
|
||||
journal_entry.flags.ignore_validate_update_after_submit = True
|
||||
journal_entry.save()
|
||||
|
||||
journal_entry.docstatus = 2
|
||||
journal_entry.make_gl_entries(1)
|
||||
journal_entry.docstatus = 1
|
||||
journal_entry.make_gl_entries()
|
||||
|
||||
|
||||
def adjust_existing_accounts(journal_entry, old_asset_name, depreciation_amount, entries_to_add):
|
||||
"""Adjust existing accounts and prepare new entries for the new asset."""
|
||||
for account in journal_entry.get("accounts"):
|
||||
if account.reference_name == old_asset_name:
|
||||
entries_to_add.append(frappe.copy_doc(account).as_dict())
|
||||
adjust_account_balance(account, depreciation_amount)
|
||||
|
||||
|
||||
def adjust_account_balance(account, depreciation_amount):
|
||||
"""Adjust the balance of an account based on the depreciation amount."""
|
||||
if account.credit:
|
||||
account.credit -= depreciation_amount
|
||||
account.credit_in_account_currency -= account.exchange_rate * depreciation_amount
|
||||
elif account.debit:
|
||||
account.debit -= depreciation_amount
|
||||
account.debit_in_account_currency -= account.exchange_rate * depreciation_amount
|
||||
|
||||
|
||||
def add_new_entries(journal_entry, entries_to_add, new_asset_name, depreciation_amount):
|
||||
"""Add new entries for the new asset to the journal entry."""
|
||||
idx = len(journal_entry.get("accounts")) + 1
|
||||
for entry in entries_to_add:
|
||||
entry.reference_name = new_asset_name
|
||||
if entry.credit:
|
||||
entry.credit = depreciation_amount
|
||||
entry.credit_in_account_currency = entry.exchange_rate * depreciation_amount
|
||||
elif entry.debit:
|
||||
entry.debit = depreciation_amount
|
||||
entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount
|
||||
entry.idx = idx
|
||||
idx += 1
|
||||
journal_entry.append("accounts", entry)
|
||||
@@ -18,8 +18,6 @@ from frappe.utils.data import add_to_date
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
make_sales_invoice,
|
||||
split_asset,
|
||||
update_maintenance_status,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
@@ -27,11 +25,15 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
restore_asset,
|
||||
scrap_asset,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.mapper import (
|
||||
make_sales_invoice,
|
||||
split_asset,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_invoice as make_invoice,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -12,8 +12,6 @@ from frappe.utils import cint, flt, get_link_to_form
|
||||
import erpnext
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_value_after_depreciation_on_disposal_date,
|
||||
reset_depreciation_schedule,
|
||||
reverse_depreciation_entry_made_on_disposal,
|
||||
@@ -396,30 +394,11 @@ class AssetCapitalization(StockController):
|
||||
def get_gl_entries(
|
||||
self, inventory_account_map=None, default_expense_account=None, default_cost_center=None
|
||||
):
|
||||
# Stock GL Entries
|
||||
gl_entries = []
|
||||
|
||||
self.inventory_account_map = inventory_account_map
|
||||
if not self.inventory_account_map:
|
||||
self.inventory_account_map = self.get_inventory_account_map()
|
||||
|
||||
precision = self.get_debit_field_precision()
|
||||
self.sle_map = self.get_stock_ledger_details()
|
||||
|
||||
target_account = self.get_target_account()
|
||||
target_against = set()
|
||||
|
||||
self.get_gl_entries_for_consumed_stock_items(gl_entries, target_account, target_against, precision)
|
||||
self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision)
|
||||
self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision)
|
||||
|
||||
composite_component_value = self.get_composite_component_value()
|
||||
|
||||
self.get_gl_entries_for_target_item(
|
||||
gl_entries, target_account, target_against, precision, composite_component_value
|
||||
from erpnext.assets.doctype.asset_capitalization.services.gl_composer import (
|
||||
AssetCapitalizationGLComposer,
|
||||
)
|
||||
|
||||
return gl_entries
|
||||
return AssetCapitalizationGLComposer(self).compose(inventory_account_map)
|
||||
|
||||
def get_target_account(self):
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
@@ -435,91 +414,6 @@ class AssetCapitalization(StockController):
|
||||
else:
|
||||
return self.target_fixed_asset_account
|
||||
|
||||
def get_gl_entries_for_consumed_stock_items(self, gl_entries, target_account, target_against, precision):
|
||||
# Consumed Stock Items
|
||||
for item_row in self.stock_items:
|
||||
sle_list = self.sle_map.get(item_row.name)
|
||||
if sle_list:
|
||||
_inv_dict = self.get_inventory_account_dict(item_row, self.inventory_account_map)
|
||||
for sle in sle_list:
|
||||
stock_value_difference = flt(sle.stock_value_difference, precision)
|
||||
|
||||
if erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
account = _inv_dict["account"]
|
||||
else:
|
||||
account = self.get_company_default("default_expense_account")
|
||||
|
||||
target_against.add(account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": target_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": -1 * stock_value_difference,
|
||||
},
|
||||
_inv_dict["account_currency"],
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_consumed_asset_items(self, gl_entries, target_account, target_against, precision):
|
||||
# Consumed Assets
|
||||
for item in self.asset_items:
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
|
||||
if asset.asset_type != "Composite Component":
|
||||
if asset.calculate_depreciation:
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset,
|
||||
item.asset_value,
|
||||
item.get("finance_book") or self.get("finance_book"),
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = target_account
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
target_against.add(gle["account"])
|
||||
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
self.set_consumed_asset_status(asset)
|
||||
|
||||
def get_gl_entries_for_consumed_service_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
# Service Expenses
|
||||
for item_row in self.service_items:
|
||||
expense_amount = flt(item_row.amount, precision)
|
||||
target_against.add(item_row.expense_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item_row.expense_account,
|
||||
"against": target_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": expense_amount,
|
||||
},
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
def get_composite_component_value(self):
|
||||
composite_component_value = 0
|
||||
for item in self.asset_items:
|
||||
@@ -528,25 +422,6 @@ class AssetCapitalization(StockController):
|
||||
composite_component_value += flt(item.asset_value, item.precision("asset_value"))
|
||||
return composite_component_value
|
||||
|
||||
def get_gl_entries_for_target_item(
|
||||
self, gl_entries, target_account, target_against, precision, composite_component_value
|
||||
):
|
||||
total_value = flt(self.total_value - composite_component_value, precision)
|
||||
if total_value:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": total_value,
|
||||
"cost_center": self.get("cost_center"),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def update_target_asset(self):
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
)
|
||||
from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
|
||||
|
||||
|
||||
class AssetCapitalizationGLComposer(BaseStockGLComposer):
|
||||
"""GL composer for Asset Capitalization.
|
||||
|
||||
Builds GL entries for consumed stock items, consumed asset items (with
|
||||
depreciation side-effects), consumed service items, and the target asset debit.
|
||||
"""
|
||||
|
||||
def compose(
|
||||
self,
|
||||
inventory_account_map: dict | None = None,
|
||||
default_expense_account: str | None = None,
|
||||
default_cost_center: str | None = None,
|
||||
) -> list:
|
||||
doc = self.doc
|
||||
gl_entries = []
|
||||
|
||||
self.inventory_account_map = inventory_account_map or doc.get_inventory_account_map()
|
||||
self.precision = doc.get_debit_field_precision()
|
||||
self.sle_map = doc.get_stock_ledger_details()
|
||||
|
||||
target_account = doc.get_target_account()
|
||||
target_against: set = set()
|
||||
|
||||
self._get_gl_entries_for_consumed_stock_items(gl_entries, target_account, target_against)
|
||||
self._get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against)
|
||||
self._get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against)
|
||||
|
||||
composite_component_value = doc.get_composite_component_value()
|
||||
self._get_gl_entries_for_target_item(
|
||||
gl_entries, target_account, target_against, composite_component_value
|
||||
)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def _get_gl_entries_for_consumed_stock_items(
|
||||
self, gl_entries: list, target_account: str, target_against: set
|
||||
) -> None:
|
||||
doc = self.doc
|
||||
for item_row in doc.stock_items:
|
||||
sle_list = self.sle_map.get(item_row.name)
|
||||
if sle_list:
|
||||
_inv_dict = doc.get_inventory_account_dict(item_row, self.inventory_account_map)
|
||||
for sle in sle_list:
|
||||
stock_value_difference = flt(sle.stock_value_difference, self.precision)
|
||||
|
||||
if erpnext.is_perpetual_inventory_enabled(doc.company):
|
||||
account = _inv_dict["account"]
|
||||
else:
|
||||
account = doc.get_company_default("default_expense_account")
|
||||
|
||||
target_against.add(account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": target_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.get("project") or doc.get("project"),
|
||||
"remarks": doc.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": -1 * stock_value_difference,
|
||||
},
|
||||
_inv_dict["account_currency"],
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
def _get_gl_entries_for_consumed_asset_items(
|
||||
self, gl_entries: list, target_account: str, target_against: set
|
||||
) -> None:
|
||||
doc = self.doc
|
||||
for item in doc.asset_items:
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
|
||||
if asset.asset_type != "Composite Component":
|
||||
if asset.calculate_depreciation:
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
||||
).format(
|
||||
frappe.utils.get_link_to_form(asset.doctype, asset.name),
|
||||
frappe.utils.get_link_to_form(doc.doctype, doc.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, doc.posting_date, notes)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset,
|
||||
item.asset_value,
|
||||
item.get("finance_book") or doc.get("finance_book"),
|
||||
doc.get("doctype"),
|
||||
doc.get("name"),
|
||||
doc.get("posting_date"),
|
||||
)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = target_account
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
target_against.add(gle["account"])
|
||||
|
||||
asset.db_set("disposal_date", doc.posting_date)
|
||||
doc.set_consumed_asset_status(asset)
|
||||
|
||||
def _get_gl_entries_for_consumed_service_items(
|
||||
self, gl_entries: list, target_account: str, target_against: set
|
||||
) -> None:
|
||||
doc = self.doc
|
||||
for item_row in doc.service_items:
|
||||
expense_amount = flt(item_row.amount, self.precision)
|
||||
target_against.add(item_row.expense_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item_row.expense_account,
|
||||
"against": target_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.get("project") or doc.get("project"),
|
||||
"remarks": doc.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": expense_amount,
|
||||
},
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
def _get_gl_entries_for_target_item(
|
||||
self,
|
||||
gl_entries: list,
|
||||
target_account: str,
|
||||
target_against: set,
|
||||
composite_component_value: float,
|
||||
) -> None:
|
||||
doc = self.doc
|
||||
total_value = flt(doc.total_value - composite_component_value, self.precision)
|
||||
if total_value:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": total_value,
|
||||
"cost_center": doc.get("cost_center"),
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
@@ -12,7 +12,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
reschedule_depreciation,
|
||||
@@ -216,9 +215,10 @@ class AssetRepair(AccountsController):
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", sabb)
|
||||
doc.cancel()
|
||||
|
||||
def on_cancel(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
def on_cancel(self): # nosemgrep
|
||||
if self.get("capitalize_repair_cost"):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.asset_doc = frappe.get_lazy_doc("Asset", self.asset)
|
||||
self.update_asset_value()
|
||||
self.make_gl_entries(cancel=True)
|
||||
self.set_increase_in_asset_life()
|
||||
@@ -307,121 +307,14 @@ class AssetRepair(AccountsController):
|
||||
)
|
||||
|
||||
def make_gl_entries(self, cancel=False):
|
||||
if cancel:
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
|
||||
if flt(self.total_repair_cost) > 0:
|
||||
gl_entries = self.get_gl_entries()
|
||||
make_gl_entries(gl_entries, cancel)
|
||||
|
||||
def get_gl_entries(self):
|
||||
gl_entries = []
|
||||
from erpnext.assets.doctype.asset_repair.services.gl_composer import AssetRepairGLComposer
|
||||
|
||||
fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company)
|
||||
self.get_gl_entries_for_repair_cost(gl_entries, fixed_asset_account)
|
||||
self.get_gl_entries_for_consumed_items(gl_entries, fixed_asset_account)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_gl_entries_for_repair_cost(self, gl_entries, fixed_asset_account):
|
||||
if flt(self.repair_cost) <= 0:
|
||||
return
|
||||
|
||||
debit_against_account = set()
|
||||
|
||||
for pi in self.invoices:
|
||||
debit_against_account.add(pi.expense_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": pi.expense_account,
|
||||
"credit": pi.repair_cost,
|
||||
"credit_in_account_currency": pi.repair_cost,
|
||||
"against": fixed_asset_account,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
debit_against_account = ", ".join(debit_against_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit": self.repair_cost,
|
||||
"debit_in_account_currency": self.repair_cost,
|
||||
"against": debit_against_account,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Asset",
|
||||
"against_voucher": self.asset,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_consumed_items(self, gl_entries, fixed_asset_account):
|
||||
if not self.get("stock_items"):
|
||||
return
|
||||
|
||||
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
|
||||
stock_entry_name = frappe.db.get_value("Stock Entry", {"asset_repair": self.name}, "name")
|
||||
stock_entry_items = frappe.get_all(
|
||||
"Stock Entry Detail", filters={"parent": stock_entry_name}, fields=["expense_account", "amount"]
|
||||
)
|
||||
|
||||
default_expense_account = None
|
||||
if not erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
default_expense_account = frappe.get_cached_value(
|
||||
"Company", self.company, "default_expense_account"
|
||||
)
|
||||
if not default_expense_account:
|
||||
frappe.throw(_("Please set default Expense Account in Company {0}").format(self.company))
|
||||
|
||||
for item in stock_entry_items:
|
||||
if flt(item.amount) > 0:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account or default_expense_account,
|
||||
"credit": item.amount,
|
||||
"credit_in_account_currency": item.amount,
|
||||
"against": fixed_asset_account,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit": item.amount,
|
||||
"debit_in_account_currency": item.amount,
|
||||
"against": item.expense_account or default_expense_account,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Stock Entry",
|
||||
"against_voucher": stock_entry_name,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
return AssetRepairGLComposer(self).compose()
|
||||
|
||||
def set_increase_in_asset_life(self):
|
||||
if self.asset_doc.calculate_depreciation and cint(self.increase_in_asset_life) > 0:
|
||||
|
||||
130
erpnext/assets/doctype/asset_repair/services/gl_composer.py
Normal file
130
erpnext/assets/doctype/asset_repair/services/gl_composer.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.services.base_gl_composer import BaseGLComposer
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account
|
||||
|
||||
|
||||
class AssetRepairGLComposer(BaseGLComposer):
|
||||
"""GL composer for Asset Repair.
|
||||
|
||||
Builds GL entries for repair cost (per invoice) and consumed stock items
|
||||
(sourced from the related Stock Entry).
|
||||
"""
|
||||
|
||||
def compose(self) -> list:
|
||||
doc = self.doc
|
||||
gl_entries = []
|
||||
|
||||
fixed_asset_account = get_asset_account("fixed_asset_account", asset=doc.asset, company=doc.company)
|
||||
self._get_gl_entries_for_repair_cost(gl_entries, fixed_asset_account)
|
||||
self._get_gl_entries_for_consumed_items(gl_entries, fixed_asset_account)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def _get_gl_entries_for_repair_cost(self, gl_entries: list, fixed_asset_account: str) -> None:
|
||||
doc = self.doc
|
||||
if flt(doc.repair_cost) <= 0:
|
||||
return
|
||||
|
||||
debit_against_account = set()
|
||||
|
||||
for pi in doc.invoices:
|
||||
debit_against_account.add(pi.expense_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": pi.expense_account,
|
||||
"credit": pi.repair_cost,
|
||||
"credit_in_account_currency": pi.repair_cost,
|
||||
"against": fixed_asset_account,
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"cost_center": doc.cost_center,
|
||||
"posting_date": doc.completion_date,
|
||||
"company": doc.company,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
debit_against_account_str = ", ".join(debit_against_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit": doc.repair_cost,
|
||||
"debit_in_account_currency": doc.repair_cost,
|
||||
"against": debit_against_account_str,
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"cost_center": doc.cost_center,
|
||||
"posting_date": doc.completion_date,
|
||||
"against_voucher_type": "Asset",
|
||||
"against_voucher": doc.asset,
|
||||
"company": doc.company,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
def _get_gl_entries_for_consumed_items(self, gl_entries: list, fixed_asset_account: str) -> None:
|
||||
doc = self.doc
|
||||
if not doc.get("stock_items"):
|
||||
return
|
||||
|
||||
stock_entry_name = frappe.db.get_value("Stock Entry", {"asset_repair": doc.name}, "name")
|
||||
stock_entry_items = frappe.get_all(
|
||||
"Stock Entry Detail", filters={"parent": stock_entry_name}, fields=["expense_account", "amount"]
|
||||
)
|
||||
|
||||
default_expense_account = None
|
||||
if not erpnext.is_perpetual_inventory_enabled(doc.company):
|
||||
default_expense_account = frappe.get_cached_value(
|
||||
"Company", doc.company, "default_expense_account"
|
||||
)
|
||||
if not default_expense_account:
|
||||
frappe.throw(_("Please set default Expense Account in Company {0}").format(doc.company))
|
||||
|
||||
for item in stock_entry_items:
|
||||
if flt(item.amount) > 0:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account or default_expense_account,
|
||||
"credit": item.amount,
|
||||
"credit_in_account_currency": item.amount,
|
||||
"against": fixed_asset_account,
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"cost_center": doc.cost_center,
|
||||
"posting_date": doc.completion_date,
|
||||
"company": doc.company,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit": item.amount,
|
||||
"debit_in_account_currency": item.amount,
|
||||
"against": item.expense_account or default_expense_account,
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"cost_center": doc.cost_center,
|
||||
"posting_date": doc.completion_date,
|
||||
"against_voucher_type": "Stock Entry",
|
||||
"against_voucher": stock_entry_name,
|
||||
"company": doc.company,
|
||||
},
|
||||
item=doc,
|
||||
)
|
||||
)
|
||||
@@ -9,6 +9,8 @@ from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowt
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
get_asset_value_after_depreciation,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.mapper import (
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.test_asset import (
|
||||
|
||||
330
erpnext/buying/doctype/purchase_order/mapper.py
Normal file
330
erpnext/buying/doctype/purchase_order/mapper.py
Normal file
@@ -0,0 +1,330 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
target.run_method("set_use_serial_batch_fields")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(
|
||||
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
|
||||
):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items")
|
||||
|
||||
def is_unit_price_row(source):
|
||||
return has_unit_price_items and source.qty == 0
|
||||
|
||||
def get_max_receivable_qty(source):
|
||||
tolerance = flt(get_allowance_for(source.item_code, qty_or_amount="qty")[0])
|
||||
return flt(source.qty) * (100 + tolerance) / 100
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
received_qty = flt(obj.received_qty)
|
||||
qty = flt(obj.qty)
|
||||
pending_qty = qty - received_qty
|
||||
|
||||
if is_unit_price_row(obj):
|
||||
target.qty = qty
|
||||
elif pending_qty > 0:
|
||||
target.qty = pending_qty
|
||||
else:
|
||||
target.qty = max(get_max_receivable_qty(obj) - received_qty, 0)
|
||||
|
||||
target.stock_qty = target.qty * flt(obj.conversion_factor)
|
||||
target.amount = target.qty * flt(obj.rate)
|
||||
target.base_amount = target.qty * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Order",
|
||||
source_name,
|
||||
{
|
||||
"Purchase Order": {
|
||||
"doctype": "Purchase Receipt",
|
||||
"field_map": {"supplier_warehouse": "supplier_warehouse"},
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"doctype": "Purchase Receipt Item",
|
||||
"field_map": {
|
||||
"name": "purchase_order_item",
|
||||
"parent": "purchase_order",
|
||||
"bom": "bom",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"sales_order": "sales_order",
|
||||
"sales_order_item": "sales_order_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (
|
||||
True
|
||||
if is_unit_price_row(doc)
|
||||
else abs(doc.received_qty) < abs(get_max_receivable_qty(doc))
|
||||
)
|
||||
and doc.delivered_by_supplier != 1
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(
|
||||
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
|
||||
):
|
||||
return get_mapped_purchase_invoice(source_name, target_doc, args=args)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice_from_portal(purchase_order_name: str):
|
||||
doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True)
|
||||
if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
doc.save()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
frappe.response["type"] = "redirect"
|
||||
frappe.response.location = "/purchase-invoices/" + doc.name
|
||||
|
||||
|
||||
def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def postprocess(source, target):
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
set_missing_values(source, target)
|
||||
|
||||
# Get the advance paid Journal Entries in Purchase Invoice Advance
|
||||
if target.get("allocate_advances_automatically"):
|
||||
target.set_advances()
|
||||
|
||||
from erpnext.accounts.services.payment_schedule import PaymentScheduleService
|
||||
|
||||
PaymentScheduleService(target).set_payment_schedule()
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
item = get_item_defaults(target.item_code, source_parent.company)
|
||||
item_group = get_item_group_defaults(target.item_code, source_parent.company)
|
||||
target.cost_center = (
|
||||
obj.cost_center
|
||||
or frappe.db.get_value("Project", obj.project, "cost_center")
|
||||
or item.get("buying_cost_center")
|
||||
or item_group.get("buying_cost_center")
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
fields = {
|
||||
"Purchase Order": {
|
||||
"doctype": "Purchase Invoice",
|
||||
"field_map": {
|
||||
"party_account_currency": "party_account_currency",
|
||||
"supplier_warehouse": "supplier_warehouse",
|
||||
},
|
||||
"field_no_map": ["payment_terms_template"],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"doctype": "Purchase Invoice Item",
|
||||
"field_map": {
|
||||
"name": "po_detail",
|
||||
"parent": "purchase_order",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (
|
||||
doc.base_amount == 0
|
||||
or abs(doc.billed_amt) < abs(doc.amount)
|
||||
or doc.qty > flt(get_billed_qty(doc.name))
|
||||
)
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
}
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Order",
|
||||
source_name,
|
||||
fields,
|
||||
target_doc,
|
||||
postprocess,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_inter_company_sales_order(source_name: str, target_doc: str | Document | None = None):
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_transaction
|
||||
|
||||
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_order(
|
||||
source_name: str,
|
||||
target_doc: str | Document | None = None,
|
||||
save: bool = False,
|
||||
submit: bool = False,
|
||||
notify: bool = False,
|
||||
):
|
||||
if not is_po_fully_subcontracted(source_name):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
else:
|
||||
frappe.throw(_("This Purchase Order has been fully subcontracted."))
|
||||
|
||||
|
||||
def is_po_fully_subcontracted(po_name: str) -> bool:
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.subcontracted_qty))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_mapped_subcontracting_order(source_name: str, target_doc: str | Document | None = None) -> Document:
|
||||
def post_process(source_doc, target_doc):
|
||||
target_doc.populate_items_table()
|
||||
|
||||
if target_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = target_doc.set_warehouse
|
||||
else:
|
||||
if source_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = source_doc.set_warehouse
|
||||
else:
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.warehouse = source_doc.items[idx].warehouse
|
||||
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.job_card = source_doc.items[idx].job_card
|
||||
if not target_doc.supplier_warehouse:
|
||||
# WIP warehouse is set as Supplier Warehouse in Job Card
|
||||
target_doc.supplier_warehouse = frappe.get_cached_value(
|
||||
"Job Card", item.job_card, "wip_warehouse"
|
||||
)
|
||||
|
||||
production_plan = set([item.production_plan for item in source_doc.items if item.production_plan])
|
||||
if production_plan:
|
||||
target_doc.production_plan = production_plan.pop()
|
||||
target_doc.reserve_stock = frappe.get_single_value(
|
||||
"Stock Settings", "auto_reserve_stock"
|
||||
) or frappe.get_value("Production Plan", target_doc.production_plan, "reserve_stock")
|
||||
|
||||
if target_doc and isinstance(target_doc, str):
|
||||
target_doc = json.loads(target_doc)
|
||||
for key in ["service_items", "items", "supplied_items"]:
|
||||
if key in target_doc:
|
||||
del target_doc[key]
|
||||
target_doc = json.dumps(target_doc)
|
||||
|
||||
target_doc = get_mapped_doc(
|
||||
"Purchase Order",
|
||||
source_name,
|
||||
{
|
||||
"Purchase Order": {
|
||||
"doctype": "Subcontracting Order",
|
||||
"field_map": {},
|
||||
"field_no_map": ["total_qty", "total", "net_total"],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"doctype": "Subcontracting Order Service Item",
|
||||
"field_map": {
|
||||
"name": "purchase_order_item",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.subcontracted_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
post_process,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
@@ -459,14 +459,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
|
||||
make_inter_company_order(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order",
|
||||
method: "erpnext.buying.doctype.purchase_order.mapper.make_inter_company_sales_order",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_purchase_receipt() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
|
||||
method: "erpnext.buying.doctype.purchase_order.mapper.make_purchase_receipt",
|
||||
frm: this.frm,
|
||||
freeze_message: __("Creating Purchase Receipt ..."),
|
||||
});
|
||||
@@ -474,14 +474,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
|
||||
make_purchase_invoice() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice",
|
||||
method: "erpnext.buying.doctype.purchase_order.mapper.make_purchase_invoice",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_subcontracting_order() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order",
|
||||
method: "erpnext.buying.doctype.purchase_order.mapper.make_subcontracting_order",
|
||||
frm: this.frm,
|
||||
freeze_message: __("Creating Subcontracting Order ..."),
|
||||
});
|
||||
@@ -493,7 +493,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
__("Material Request"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order",
|
||||
method: "erpnext.stock.doctype.material_request.mapper.make_purchase_order",
|
||||
source_doctype: "Material Request",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
@@ -518,7 +518,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
__("Supplier Quotation"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.buying.doctype.supplier_quotation.supplier_quotation.make_purchase_order",
|
||||
method: "erpnext.buying.doctype.supplier_quotation.mapper.make_purchase_order",
|
||||
source_doctype: "Supplier Quotation",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
|
||||
@@ -8,28 +8,30 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form
|
||||
from frappe.utils import cint, cstr, flt
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
unlink_inter_company_doc,
|
||||
update_linked_doc,
|
||||
validate_inter_company_party,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account, get_party_account_currency
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
from erpnext.buying.utils import validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
)
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
|
||||
from erpnext.stock.doctype.item.item import get_last_purchase_details
|
||||
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
|
||||
from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
|
||||
get_subcontracting_boms_for_finished_goods,
|
||||
)
|
||||
|
||||
from .mapper import (
|
||||
make_subcontracting_order,
|
||||
)
|
||||
|
||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
|
||||
@@ -742,201 +744,6 @@ def close_or_unclose_purchase_orders(names: str, status: str):
|
||||
frappe.local.message_log = []
|
||||
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
target.run_method("set_use_serial_batch_fields")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(
|
||||
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
|
||||
):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items")
|
||||
|
||||
def is_unit_price_row(source):
|
||||
return has_unit_price_items and source.qty == 0
|
||||
|
||||
def get_max_receivable_qty(source):
|
||||
tolerance = flt(get_allowance_for(source.item_code, qty_or_amount="qty")[0])
|
||||
return flt(source.qty) * (100 + tolerance) / 100
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
received_qty = flt(obj.received_qty)
|
||||
qty = flt(obj.qty)
|
||||
pending_qty = qty - received_qty
|
||||
|
||||
if is_unit_price_row(obj):
|
||||
target.qty = qty
|
||||
elif pending_qty > 0:
|
||||
target.qty = pending_qty
|
||||
else:
|
||||
target.qty = max(get_max_receivable_qty(obj) - received_qty, 0)
|
||||
|
||||
target.stock_qty = target.qty * flt(obj.conversion_factor)
|
||||
target.amount = target.qty * flt(obj.rate)
|
||||
target.base_amount = target.qty * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Order",
|
||||
source_name,
|
||||
{
|
||||
"Purchase Order": {
|
||||
"doctype": "Purchase Receipt",
|
||||
"field_map": {"supplier_warehouse": "supplier_warehouse"},
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"doctype": "Purchase Receipt Item",
|
||||
"field_map": {
|
||||
"name": "purchase_order_item",
|
||||
"parent": "purchase_order",
|
||||
"bom": "bom",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"sales_order": "sales_order",
|
||||
"sales_order_item": "sales_order_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (
|
||||
True
|
||||
if is_unit_price_row(doc)
|
||||
else abs(doc.received_qty) < abs(get_max_receivable_qty(doc))
|
||||
)
|
||||
and doc.delivered_by_supplier != 1
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(
|
||||
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
|
||||
):
|
||||
return get_mapped_purchase_invoice(source_name, target_doc, args=args)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice_from_portal(purchase_order_name: str):
|
||||
doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True)
|
||||
if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
doc.save()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
frappe.response["type"] = "redirect"
|
||||
frappe.response.location = "/purchase-invoices/" + doc.name
|
||||
|
||||
|
||||
def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def postprocess(source, target):
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
set_missing_values(source, target)
|
||||
|
||||
# Get the advance paid Journal Entries in Purchase Invoice Advance
|
||||
if target.get("allocate_advances_automatically"):
|
||||
target.set_advances()
|
||||
|
||||
target.set_payment_schedule()
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
item = get_item_defaults(target.item_code, source_parent.company)
|
||||
item_group = get_item_group_defaults(target.item_code, source_parent.company)
|
||||
target.cost_center = (
|
||||
obj.cost_center
|
||||
or frappe.db.get_value("Project", obj.project, "cost_center")
|
||||
or item.get("buying_cost_center")
|
||||
or item_group.get("buying_cost_center")
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
fields = {
|
||||
"Purchase Order": {
|
||||
"doctype": "Purchase Invoice",
|
||||
"field_map": {
|
||||
"party_account_currency": "party_account_currency",
|
||||
"supplier_warehouse": "supplier_warehouse",
|
||||
},
|
||||
"field_no_map": ["payment_terms_template"],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"doctype": "Purchase Invoice Item",
|
||||
"field_map": {
|
||||
"name": "po_detail",
|
||||
"parent": "purchase_order",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (
|
||||
doc.base_amount == 0
|
||||
or abs(doc.billed_amt) < abs(doc.amount)
|
||||
or doc.qty > flt(get_billed_qty(doc.name))
|
||||
)
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
}
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Order",
|
||||
source_name,
|
||||
fields,
|
||||
target_doc,
|
||||
postprocess,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||
|
||||
@@ -958,121 +765,3 @@ def update_status(status: str, name: str):
|
||||
po = frappe.get_lazy_doc("Purchase Order", name, check_permission="submit")
|
||||
po.update_status(status)
|
||||
po.update_delivered_qty_in_sales_order()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_inter_company_sales_order(source_name: str, target_doc: str | Document | None = None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
|
||||
|
||||
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_order(
|
||||
source_name: str,
|
||||
target_doc: str | Document | None = None,
|
||||
save: bool = False,
|
||||
submit: bool = False,
|
||||
notify: bool = False,
|
||||
):
|
||||
if not is_po_fully_subcontracted(source_name):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
else:
|
||||
frappe.throw(_("This Purchase Order has been fully subcontracted."))
|
||||
|
||||
|
||||
def is_po_fully_subcontracted(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.subcontracted_qty))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
def post_process(source_doc, target_doc):
|
||||
target_doc.populate_items_table()
|
||||
|
||||
if target_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = target_doc.set_warehouse
|
||||
else:
|
||||
if source_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = source_doc.set_warehouse
|
||||
else:
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.warehouse = source_doc.items[idx].warehouse
|
||||
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.job_card = source_doc.items[idx].job_card
|
||||
if not target_doc.supplier_warehouse:
|
||||
# WIP warehouse is set as Supplier Warehouse in Job Card
|
||||
target_doc.supplier_warehouse = frappe.get_cached_value(
|
||||
"Job Card", item.job_card, "wip_warehouse"
|
||||
)
|
||||
|
||||
production_plan = set([item.production_plan for item in source_doc.items if item.production_plan])
|
||||
if production_plan:
|
||||
target_doc.production_plan = production_plan.pop()
|
||||
target_doc.reserve_stock = frappe.get_single_value(
|
||||
"Stock Settings", "auto_reserve_stock"
|
||||
) or frappe.get_value("Production Plan", target_doc.production_plan, "reserve_stock")
|
||||
|
||||
if target_doc and isinstance(target_doc, str):
|
||||
target_doc = json.loads(target_doc)
|
||||
for key in ["service_items", "items", "supplied_items"]:
|
||||
if key in target_doc:
|
||||
del target_doc[key]
|
||||
target_doc = json.dumps(target_doc)
|
||||
|
||||
target_doc = get_mapped_doc(
|
||||
"Purchase Order",
|
||||
source_name,
|
||||
{
|
||||
"Purchase Order": {
|
||||
"doctype": "Subcontracting Order",
|
||||
"field_map": {},
|
||||
"field_no_map": ["total_qty", "total", "net_total"],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"doctype": "Subcontracting Order Service Item",
|
||||
"field_map": {
|
||||
"name": "purchase_order_item",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.subcontracted_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
post_process,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
@@ -11,19 +11,19 @@ from frappe.utils.data import today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.party import get_due_date_from_template
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
from erpnext.buying.doctype.purchase_order.mapper import (
|
||||
make_inter_company_sales_order,
|
||||
make_purchase_receipt,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
from erpnext.buying.doctype.purchase_order.mapper import (
|
||||
make_purchase_invoice as make_pi_from_po,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.mapper import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.test_material_request import make_material_request
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
@@ -105,7 +105,7 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
||||
Regression test for #55246: the mapper dropped rows once
|
||||
received_qty >= qty, ignoring the configured tolerance.
|
||||
"""
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
|
||||
# 50% tolerance — 10 ordered allows up to 15 received
|
||||
frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50)
|
||||
@@ -611,7 +611,7 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
||||
self.assertEqual(po.get("items")[0].received_qty, 5)
|
||||
|
||||
def test_purchase_order_invoice_receipt_workflow(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt
|
||||
from erpnext.accounts.doctype.purchase_invoice.mapper import make_purchase_receipt
|
||||
|
||||
po = create_purchase_order()
|
||||
pi = make_pi_from_po(po.name)
|
||||
@@ -1050,14 +1050,14 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
||||
|
||||
def test_internal_transfer_flow(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import (
|
||||
make_inter_company_purchase_invoice,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
from erpnext.selling.doctype.sales_order.mapper import (
|
||||
make_delivery_note,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
|
||||
|
||||
frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", 1)
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
@@ -1198,7 +1198,7 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
||||
self.assertEqual(po.items[0].fg_item_qty, 30)
|
||||
|
||||
def test_new_sc_flow(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_subcontracting_order
|
||||
|
||||
po = create_po_for_sc_testing()
|
||||
sco = make_subcontracting_order(po.name)
|
||||
@@ -1326,7 +1326,7 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated")
|
||||
|
||||
def test_po_billed_amount_against_return_entry(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note
|
||||
from erpnext.accounts.doctype.purchase_invoice.mapper import make_debit_note
|
||||
|
||||
# Create a Purchase Order and Fully Bill it
|
||||
po = create_purchase_order()
|
||||
|
||||
185
erpnext/buying/doctype/request_for_quotation/mapper.py
Normal file
185
erpnext/buying/doctype/request_for_quotation/mapper.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.accounts.party import _get_party_details, get_party_account_currency
|
||||
from erpnext.stock.doctype.material_request.mapper import set_missing_values
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_supplier_quotation_from_rfq(
|
||||
source_name: str, target_doc: str | Document | None = None, for_supplier: str | None = None
|
||||
):
|
||||
def postprocess(source, target_doc):
|
||||
if for_supplier:
|
||||
target_doc.supplier = for_supplier
|
||||
args = _get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True)
|
||||
target_doc.currency = args.currency or get_party_account_currency(
|
||||
"Supplier", for_supplier, source.company
|
||||
)
|
||||
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_single_value(
|
||||
"Buying Settings", "buying_price_list"
|
||||
)
|
||||
set_missing_values(source, target_doc)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Request for Quotation",
|
||||
source_name,
|
||||
{
|
||||
"Request for Quotation": {
|
||||
"doctype": "Supplier Quotation",
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
"field_map": {"opportunity": "opportunity"},
|
||||
},
|
||||
"Request for Quotation Item": {
|
||||
"doctype": "Supplier Quotation Item",
|
||||
"field_map": {
|
||||
"name": "request_for_quotation_item",
|
||||
"parent": "request_for_quotation",
|
||||
"project_name": "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
postprocess,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
# This method is used to make supplier quotation from supplier's portal.
|
||||
@frappe.whitelist()
|
||||
def create_supplier_quotation(doc: str | Document | dict):
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if frappe.session.user not in frappe.get_all(
|
||||
"Portal User", {"parent": doc.get("supplier")}, pluck="user"
|
||||
):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
|
||||
try:
|
||||
sq_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Supplier Quotation",
|
||||
"supplier": doc.get("supplier"),
|
||||
"terms": doc.get("terms"),
|
||||
"company": doc.get("company"),
|
||||
"currency": doc.get("currency")
|
||||
or get_party_account_currency("Supplier", doc.get("supplier"), doc.get("company")),
|
||||
"buying_price_list": doc.get("buying_price_list")
|
||||
or frappe.db.get_single_value("Buying Settings", "buying_price_list"),
|
||||
}
|
||||
)
|
||||
add_items(sq_doc, doc.get("supplier"), doc.get("items"))
|
||||
sq_doc.flags.ignore_permissions = True
|
||||
sq_doc.run_method("set_missing_values")
|
||||
sq_doc.save()
|
||||
frappe.msgprint(_("Supplier Quotation {0} Created").format(sq_doc.name))
|
||||
return sq_doc.name
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def add_items(sq_doc, supplier, items):
|
||||
for data in items:
|
||||
if isinstance(data, dict):
|
||||
data = frappe._dict(data)
|
||||
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
|
||||
|
||||
def create_rfq_items(sq_doc, supplier, data):
|
||||
args = {}
|
||||
|
||||
for field in [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"description",
|
||||
"qty",
|
||||
"rate",
|
||||
"conversion_factor",
|
||||
"warehouse",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"stock_qty",
|
||||
"uom",
|
||||
]:
|
||||
args[field] = data.get(field)
|
||||
|
||||
args.update(
|
||||
{
|
||||
"request_for_quotation_item": data.name,
|
||||
"request_for_quotation": data.parent,
|
||||
"supplier_part_no": frappe.db.get_value(
|
||||
"Item Supplier", {"parent": data.item_code, "supplier": supplier}, "supplier_part_no"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
sq_doc.append("items", args)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_from_material_requests_based_on_supplier(
|
||||
source_name: str, target_doc: str | Document | None = None
|
||||
):
|
||||
Item = frappe.qb.DocType("Item")
|
||||
Item_Supp = frappe.qb.DocType("Item Supplier")
|
||||
MR = frappe.qb.DocType("Material Request")
|
||||
MR_Item = frappe.qb.DocType("Material Request Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(MR_Item)
|
||||
.join(MR)
|
||||
.on(MR_Item.parent == MR.name)
|
||||
.join(Item)
|
||||
.on(MR_Item.item_code == Item.name)
|
||||
.join(Item_Supp)
|
||||
.on(Item.name == Item_Supp.parent)
|
||||
.select(MR.name, MR_Item.item_code)
|
||||
.where(Item_Supp.supplier == source_name)
|
||||
.where(MR.status != "Stopped")
|
||||
.where(MR.material_request_type == "Purchase")
|
||||
.where(MR.docstatus == 1)
|
||||
.where(MR.per_ordered < 99.99)
|
||||
)
|
||||
|
||||
mr_items_list = query.run(as_dict=True)
|
||||
|
||||
material_requests = {}
|
||||
for d in mr_items_list:
|
||||
material_requests.setdefault(d.name, []).append(d.item_code)
|
||||
|
||||
for mr, items in material_requests.items():
|
||||
target_doc = get_mapped_doc(
|
||||
"Material Request",
|
||||
mr,
|
||||
{
|
||||
"Material Request": {
|
||||
"doctype": "Request for Quotation",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
"material_request_type": ["=", "Purchase"],
|
||||
},
|
||||
},
|
||||
"Material Request Item": {
|
||||
"doctype": "Request for Quotation Item",
|
||||
"condition": lambda row: row.item_code in items,
|
||||
"field_map": [
|
||||
["name", "material_request_item"],
|
||||
["parent", "material_request"],
|
||||
["uom", "uom"],
|
||||
],
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
@@ -209,7 +209,7 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
|
||||
return frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq",
|
||||
method: "erpnext.buying.doctype.request_for_quotation.mapper.make_supplier_quotation_from_rfq",
|
||||
args: {
|
||||
source_name: doc.name,
|
||||
for_supplier: args.supplier,
|
||||
@@ -361,7 +361,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
|
||||
__("Material Request"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.material_request.material_request.make_request_for_quotation",
|
||||
method: "erpnext.stock.doctype.material_request.mapper.make_request_for_quotation",
|
||||
source_doctype: "Material Request",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
@@ -385,7 +385,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
|
||||
__("Opportunity"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.crm.doctype.opportunity.opportunity.make_request_for_quotation",
|
||||
method: "erpnext.crm.doctype.opportunity.mapper.make_request_for_quotation",
|
||||
source_doctype: "Opportunity",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
@@ -425,7 +425,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
|
||||
dialog.hide();
|
||||
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_item_from_material_requests_based_on_supplier",
|
||||
method: "erpnext.buying.doctype.request_for_quotation.mapper.get_item_from_material_requests_based_on_supplier",
|
||||
source_name: args.supplier,
|
||||
target: me.frm,
|
||||
setters: {
|
||||
|
||||
@@ -2,24 +2,19 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.contact.contact import get_full_name
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.desk.form.load import get_attachments
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Order
|
||||
from frappe.utils import get_url
|
||||
from frappe.utils.print_format import download_pdf
|
||||
from frappe.utils.user import get_user_fullname
|
||||
|
||||
from erpnext.accounts.party import _get_party_details, get_party_account_currency
|
||||
from erpnext.buying.utils import validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock.doctype.material_request.material_request import set_missing_values
|
||||
|
||||
STANDARD_USERS = ("Guest", "Administrator")
|
||||
|
||||
@@ -447,120 +442,6 @@ def get_list_context(context=None):
|
||||
return list_context
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_supplier_quotation_from_rfq(
|
||||
source_name: str, target_doc: str | Document | None = None, for_supplier: str | None = None
|
||||
):
|
||||
def postprocess(source, target_doc):
|
||||
if for_supplier:
|
||||
target_doc.supplier = for_supplier
|
||||
args = _get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True)
|
||||
target_doc.currency = args.currency or get_party_account_currency(
|
||||
"Supplier", for_supplier, source.company
|
||||
)
|
||||
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_single_value(
|
||||
"Buying Settings", "buying_price_list"
|
||||
)
|
||||
set_missing_values(source, target_doc)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Request for Quotation",
|
||||
source_name,
|
||||
{
|
||||
"Request for Quotation": {
|
||||
"doctype": "Supplier Quotation",
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
"field_map": {"opportunity": "opportunity"},
|
||||
},
|
||||
"Request for Quotation Item": {
|
||||
"doctype": "Supplier Quotation Item",
|
||||
"field_map": {
|
||||
"name": "request_for_quotation_item",
|
||||
"parent": "request_for_quotation",
|
||||
"project_name": "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
postprocess,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
# This method is used to make supplier quotation from supplier's portal.
|
||||
@frappe.whitelist()
|
||||
def create_supplier_quotation(doc: str | Document | dict):
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if frappe.session.user not in frappe.get_all(
|
||||
"Portal User", {"parent": doc.get("supplier")}, pluck="user"
|
||||
):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
|
||||
try:
|
||||
sq_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Supplier Quotation",
|
||||
"supplier": doc.get("supplier"),
|
||||
"terms": doc.get("terms"),
|
||||
"company": doc.get("company"),
|
||||
"currency": doc.get("currency")
|
||||
or get_party_account_currency("Supplier", doc.get("supplier"), doc.get("company")),
|
||||
"buying_price_list": doc.get("buying_price_list")
|
||||
or frappe.db.get_single_value("Buying Settings", "buying_price_list"),
|
||||
}
|
||||
)
|
||||
add_items(sq_doc, doc.get("supplier"), doc.get("items"))
|
||||
sq_doc.flags.ignore_permissions = True
|
||||
sq_doc.run_method("set_missing_values")
|
||||
sq_doc.save()
|
||||
frappe.msgprint(_("Supplier Quotation {0} Created").format(sq_doc.name))
|
||||
return sq_doc.name
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def add_items(sq_doc, supplier, items):
|
||||
for data in items:
|
||||
if isinstance(data, dict):
|
||||
data = frappe._dict(data)
|
||||
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
|
||||
|
||||
def create_rfq_items(sq_doc, supplier, data):
|
||||
args = {}
|
||||
|
||||
for field in [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"description",
|
||||
"qty",
|
||||
"rate",
|
||||
"conversion_factor",
|
||||
"warehouse",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"stock_qty",
|
||||
"uom",
|
||||
]:
|
||||
args[field] = data.get(field)
|
||||
|
||||
args.update(
|
||||
{
|
||||
"request_for_quotation_item": data.name,
|
||||
"request_for_quotation": data.parent,
|
||||
"supplier_part_no": frappe.db.get_value(
|
||||
"Item Supplier", {"parent": data.item_code, "supplier": supplier}, "supplier_part_no"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
sq_doc.append("items", args)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pdf(
|
||||
name: str,
|
||||
@@ -584,65 +465,6 @@ def get_pdf(
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_from_material_requests_based_on_supplier(
|
||||
source_name: str, target_doc: str | Document | None = None
|
||||
):
|
||||
Item = frappe.qb.DocType("Item")
|
||||
Item_Supp = frappe.qb.DocType("Item Supplier")
|
||||
MR = frappe.qb.DocType("Material Request")
|
||||
MR_Item = frappe.qb.DocType("Material Request Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(MR_Item)
|
||||
.join(MR)
|
||||
.on(MR_Item.parent == MR.name)
|
||||
.join(Item)
|
||||
.on(MR_Item.item_code == Item.name)
|
||||
.join(Item_Supp)
|
||||
.on(Item.name == Item_Supp.parent)
|
||||
.select(MR.name, MR_Item.item_code)
|
||||
.where(Item_Supp.supplier == source_name)
|
||||
.where(MR.status != "Stopped")
|
||||
.where(MR.material_request_type == "Purchase")
|
||||
.where(MR.docstatus == 1)
|
||||
.where(MR.per_ordered < 99.99)
|
||||
)
|
||||
|
||||
mr_items_list = query.run(as_dict=True)
|
||||
|
||||
material_requests = {}
|
||||
for d in mr_items_list:
|
||||
material_requests.setdefault(d.name, []).append(d.item_code)
|
||||
|
||||
for mr, items in material_requests.items():
|
||||
target_doc = get_mapped_doc(
|
||||
"Material Request",
|
||||
mr,
|
||||
{
|
||||
"Material Request": {
|
||||
"doctype": "Request for Quotation",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
"material_request_type": ["=", "Purchase"],
|
||||
},
|
||||
},
|
||||
"Material Request Item": {
|
||||
"doctype": "Request for Quotation Item",
|
||||
"condition": lambda row: row.item_code in items,
|
||||
"field_map": [
|
||||
["name", "material_request_item"],
|
||||
["parent", "material_request"],
|
||||
["uom", "uom"],
|
||||
],
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_supplier_tag():
|
||||
filters = {"document_type": "Supplier"}
|
||||
|
||||
@@ -8,13 +8,15 @@ import frappe
|
||||
from frappe.tests import change_settings
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
|
||||
from erpnext.buying.doctype.request_for_quotation.mapper import (
|
||||
create_supplier_quotation,
|
||||
get_pdf,
|
||||
make_supplier_quotation_from_rfq,
|
||||
)
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
|
||||
get_pdf,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
|
||||
from erpnext.crm.doctype.opportunity.mapper import make_request_for_quotation as make_rfq
|
||||
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.templates.pages.rfq import check_supplier_has_docname_access
|
||||
|
||||
@@ -183,7 +183,7 @@ class Supplier(TransactionBase):
|
||||
)
|
||||
|
||||
def create_primary_contact(self):
|
||||
from erpnext.selling.doctype.customer.customer import make_contact
|
||||
from erpnext.selling.doctype.customer.mapper import make_contact
|
||||
|
||||
if not self.supplier_primary_contact:
|
||||
if self.mobile_no or self.email_id:
|
||||
@@ -195,7 +195,7 @@ class Supplier(TransactionBase):
|
||||
def create_primary_address(self):
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
|
||||
from erpnext.selling.doctype.customer.customer import make_address
|
||||
from erpnext.selling.doctype.customer.mapper import make_address
|
||||
|
||||
if self.flags.is_new_doc and self.get("address_line1"):
|
||||
address = make_address(self)
|
||||
|
||||
110
erpnext/buying/doctype/supplier_quotation/mapper.py
Normal file
110
erpnext/buying/doctype/supplier_quotation/mapper.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_order(
|
||||
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
|
||||
):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("get_schedule_dates")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Supplier Quotation",
|
||||
source_name,
|
||||
{
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Purchase Order",
|
||||
"field_no_map": ["transaction_date"],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Supplier Quotation Item": {
|
||||
"doctype": "Purchase Order Item",
|
||||
"field_map": [
|
||||
["name", "supplier_quotation_item"],
|
||||
["parent", "supplier_quotation"],
|
||||
["material_request", "material_request"],
|
||||
["material_request_item", "material_request_item"],
|
||||
["sales_order", "sales_order"],
|
||||
],
|
||||
"postprocess": update_item,
|
||||
"condition": select_item,
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(source_name: str, target_doc: str | Document | None = None):
|
||||
doc = get_mapped_doc(
|
||||
"Supplier Quotation",
|
||||
source_name,
|
||||
{
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Purchase Invoice",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Supplier Quotation Item": {"doctype": "Purchase Invoice Item"},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
doclist = get_mapped_doc(
|
||||
"Supplier Quotation",
|
||||
source_name,
|
||||
{
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Quotation",
|
||||
"field_map": {
|
||||
"name": "supplier_quotation",
|
||||
},
|
||||
},
|
||||
"Supplier Quotation Item": {
|
||||
"doctype": "Quotation Item",
|
||||
"condition": lambda doc: frappe.db.get_value("Item", doc.item_code, "is_sales_item") == 1,
|
||||
"add_if_empty": True,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doclist
|
||||
@@ -56,7 +56,7 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
__("Material Request"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.material_request.material_request.make_supplier_quotation",
|
||||
method: "erpnext.stock.doctype.material_request.mapper.make_supplier_quotation",
|
||||
source_doctype: "Material Request",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
@@ -91,7 +91,7 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
frappe.throw({ message: __("Please select a Supplier"), title: __("Mandatory") });
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq",
|
||||
method: "erpnext.buying.doctype.request_for_quotation.mapper.make_supplier_quotation_from_rfq",
|
||||
source_doctype: "Request for Quotation",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
@@ -112,13 +112,13 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
|
||||
make_purchase_order() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.buying.doctype.supplier_quotation.supplier_quotation.make_purchase_order",
|
||||
method: "erpnext.buying.doctype.supplier_quotation.mapper.make_purchase_order",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
make_quotation() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.buying.doctype.supplier_quotation.supplier_quotation.make_quotation",
|
||||
method: "erpnext.buying.doctype.supplier_quotation.mapper.make_quotation",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt, getdate, nowdate
|
||||
from frappe.utils import getdate, nowdate
|
||||
|
||||
from erpnext.buying.utils import validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
@@ -245,107 +242,6 @@ def get_list_context(context=None):
|
||||
return list_context
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_order(
|
||||
source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None
|
||||
):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("get_schedule_dates")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Supplier Quotation",
|
||||
source_name,
|
||||
{
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Purchase Order",
|
||||
"field_no_map": ["transaction_date"],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Supplier Quotation Item": {
|
||||
"doctype": "Purchase Order Item",
|
||||
"field_map": [
|
||||
["name", "supplier_quotation_item"],
|
||||
["parent", "supplier_quotation"],
|
||||
["material_request", "material_request"],
|
||||
["material_request_item", "material_request_item"],
|
||||
["sales_order", "sales_order"],
|
||||
],
|
||||
"postprocess": update_item,
|
||||
"condition": select_item,
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(source_name: str, target_doc: str | Document | None = None):
|
||||
doc = get_mapped_doc(
|
||||
"Supplier Quotation",
|
||||
source_name,
|
||||
{
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Purchase Invoice",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Supplier Quotation Item": {"doctype": "Purchase Invoice Item"},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
doclist = get_mapped_doc(
|
||||
"Supplier Quotation",
|
||||
source_name,
|
||||
{
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Quotation",
|
||||
"field_map": {
|
||||
"name": "supplier_quotation",
|
||||
},
|
||||
},
|
||||
"Supplier Quotation Item": {
|
||||
"doctype": "Quotation Item",
|
||||
"condition": lambda doc: frappe.db.get_value("Item", doc.item_code, "is_sales_item") == 1,
|
||||
"add_if_empty": True,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
def set_expired_status():
|
||||
frappe.db.set_value(
|
||||
"Supplier Quotation",
|
||||
|
||||
@@ -8,7 +8,7 @@ import frappe
|
||||
from frappe.tests import change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
from erpnext.buying.doctype.supplier_quotation.mapper import make_purchase_order
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import (
|
||||
get_data,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.mapper import make_purchase_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,6 @@ import erpnext
|
||||
from erpnext.accounts.general_ledger import (
|
||||
make_gl_entries,
|
||||
make_reverse_gl_entries,
|
||||
process_gl_map,
|
||||
)
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
@@ -691,140 +690,10 @@ class StockController(AccountsController):
|
||||
def get_gl_entries(
|
||||
self, inventory_account_map=None, default_expense_account=None, default_cost_center=None
|
||||
):
|
||||
if not inventory_account_map:
|
||||
inventory_account_map = self.get_inventory_account_map()
|
||||
from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
|
||||
|
||||
sle_map = self.get_stock_ledger_details()
|
||||
voucher_details = self.get_voucher_details(default_expense_account, default_cost_center, sle_map)
|
||||
|
||||
gl_list = []
|
||||
warehouse_with_no_account = []
|
||||
precision = self.get_debit_field_precision()
|
||||
for item_row in voucher_details:
|
||||
sle_list = sle_map.get(item_row.name)
|
||||
sle_rounding_diff = 0.0
|
||||
if sle_list:
|
||||
for sle in sle_list:
|
||||
_inv_dict = self.get_inventory_account_dict(sle, inventory_account_map)
|
||||
|
||||
if _inv_dict.get("account"):
|
||||
# from warehouse account
|
||||
|
||||
sle_rounding_diff += flt(sle.stock_value_difference)
|
||||
|
||||
self.check_expense_account(item_row)
|
||||
|
||||
# expense account/ target_warehouse / source_warehouse
|
||||
if item_row.get("target_warehouse"):
|
||||
_target_wh_inv_dict = self.get_inventory_account_dict(
|
||||
item_row, inventory_account_map, warehouse_field="target_warehouse"
|
||||
)
|
||||
expense_account = _target_wh_inv_dict["account"]
|
||||
else:
|
||||
expense_account = item_row.expense_account
|
||||
|
||||
gl_list.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": _inv_dict["account"],
|
||||
"against": expense_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": sle.get("project") or item_row.project or self.get("project"),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": flt(sle.stock_value_difference, precision),
|
||||
"is_opening": item_row.get("is_opening")
|
||||
or self.get("is_opening")
|
||||
or "No",
|
||||
},
|
||||
_inv_dict["account_currency"],
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
gl_list.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": _inv_dict["account"],
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(sle.stock_value_difference, precision),
|
||||
"project": sle.get("project")
|
||||
or item_row.get("project")
|
||||
or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening")
|
||||
or self.get("is_opening")
|
||||
or "No",
|
||||
},
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
elif sle.warehouse not in warehouse_with_no_account:
|
||||
warehouse_with_no_account.append(sle.warehouse)
|
||||
|
||||
if abs(sle_rounding_diff) > (1.0 / (10**precision)) and self.is_internal_transfer():
|
||||
warehouse_asset_account = ""
|
||||
if self.get("is_internal_customer"):
|
||||
_inv_dict = self.get_inventory_account_dict(
|
||||
item_row, inventory_account_map, warehouse_field="target_warehouse"
|
||||
)
|
||||
|
||||
warehouse_asset_account = _inv_dict.get("account") if _inv_dict else None
|
||||
elif self.get("is_internal_supplier"):
|
||||
_inv_dict = self.get_inventory_account_dict(item_row, inventory_account_map)
|
||||
|
||||
warehouse_asset_account = _inv_dict.get("account") if _inv_dict else None
|
||||
|
||||
expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account")
|
||||
if not expense_account:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer"
|
||||
).format(frappe.bold(self.company))
|
||||
)
|
||||
|
||||
gl_list.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": warehouse_asset_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.project or self.get("project"),
|
||||
"remarks": _("Rounding gain/loss Entry for Stock Transfer"),
|
||||
"debit": sle_rounding_diff,
|
||||
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
|
||||
},
|
||||
_inv_dict["account_currency"],
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
gl_list.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": warehouse_asset_account,
|
||||
"against": expense_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": _("Rounding gain/loss Entry for Stock Transfer"),
|
||||
"credit": sle_rounding_diff,
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
|
||||
},
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
if warehouse_with_no_account:
|
||||
for wh in warehouse_with_no_account:
|
||||
if frappe.get_cached_value("Warehouse", wh, "company"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}."
|
||||
).format(wh, self.company)
|
||||
)
|
||||
|
||||
return process_gl_map(
|
||||
gl_list, precision=precision, from_repost=frappe.flags.through_repost_item_valuation
|
||||
return BaseStockGLComposer(self).compose(
|
||||
inventory_account_map, default_expense_account, default_cost_center
|
||||
)
|
||||
|
||||
def get_debit_field_precision(self):
|
||||
@@ -1792,28 +1661,25 @@ class StockController(AccountsController):
|
||||
item=None,
|
||||
posting_date=None,
|
||||
):
|
||||
gl_entry = {
|
||||
"account": account,
|
||||
"cost_center": cost_center,
|
||||
"debit": debit,
|
||||
"credit": credit,
|
||||
"against": against_account,
|
||||
"remarks": remarks,
|
||||
}
|
||||
from erpnext.accounts.services.base_gl_composer import add_gl_entry
|
||||
|
||||
if voucher_detail_no:
|
||||
gl_entry.update({"voucher_detail_no": voucher_detail_no})
|
||||
|
||||
if debit_in_account_currency:
|
||||
gl_entry.update({"debit_in_account_currency": debit_in_account_currency})
|
||||
|
||||
if credit_in_account_currency:
|
||||
gl_entry.update({"credit_in_account_currency": credit_in_account_currency})
|
||||
|
||||
if posting_date:
|
||||
gl_entry.update({"posting_date": posting_date})
|
||||
|
||||
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
|
||||
add_gl_entry(
|
||||
self,
|
||||
gl_entries,
|
||||
account,
|
||||
cost_center,
|
||||
debit,
|
||||
credit,
|
||||
remarks,
|
||||
against_account,
|
||||
debit_in_account_currency,
|
||||
credit_in_account_currency,
|
||||
account_currency,
|
||||
project,
|
||||
voucher_detail_no,
|
||||
item,
|
||||
posting_date,
|
||||
)
|
||||
|
||||
def update_stock_reservation_entries(self):
|
||||
def get_sre_list():
|
||||
|
||||
@@ -810,7 +810,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
|
||||
@ERPNextTestSuite.change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1})
|
||||
def test_16_internal_transfer_at_arms_length_price(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_purchase_invoice
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
@@ -2247,7 +2247,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
Test that additional discount amount is not copied repeatedly
|
||||
when creating multiple delivery notes from a single sales order with discount_amount set
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount amount
|
||||
@@ -2283,7 +2283,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
Test that additional discount amount is not copied repeatedly
|
||||
when creating multiple purchase receipts from a single purchase order with discount_amount set
|
||||
"""
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
# Create a purchase order with discount amount
|
||||
@@ -2319,7 +2319,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
Test that discount amount is partially applied when some discount
|
||||
has already been used in previous mapped transactions
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount amount
|
||||
@@ -2357,7 +2357,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
Test that discount amount is not adjusted when additional_discount_percentage
|
||||
is set in the source document (as it will be recalculated based on percentage)
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount percentage instead of amount
|
||||
@@ -2385,7 +2385,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
Test that discount amount is correctly adjusted when multiple return invoices
|
||||
are created against the same original invoice to prevent over-returning discount
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
|
||||
|
||||
# Create original sales invoice with discount
|
||||
si = create_sales_invoice(qty=10, rate=100, do_not_submit=True)
|
||||
|
||||
@@ -6,8 +6,8 @@ import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -23,7 +23,7 @@ class TestMapper(ERPNextTestSuite):
|
||||
so, item_list_3 = self.make_sales_order()
|
||||
|
||||
# Map source docs to target with corresponding mapper method
|
||||
method = "erpnext.selling.doctype.quotation.quotation.make_sales_order"
|
||||
method = "erpnext.selling.doctype.quotation.mapper.make_sales_order"
|
||||
updated_so = mapper.map_docs(method, json.dumps([qtn1.name, qtn2.name]), so)
|
||||
|
||||
# Assert that all inserted items are present in updated sales order
|
||||
|
||||
@@ -88,14 +88,14 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
|
||||
make_customer() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_customer",
|
||||
method: "erpnext.crm.doctype.lead.mapper.make_customer",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_quotation() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_quotation",
|
||||
method: "erpnext.crm.doctype.lead.mapper.make_quotation",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
@@ -171,7 +171,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
method: "erpnext.crm.doctype.lead.mapper.make_opportunity",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
@@ -184,7 +184,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
d.show();
|
||||
} else {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
method: "erpnext.crm.doctype.lead.mapper.make_opportunity",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,18 +7,14 @@ from frappe.contacts.address_and_contact import (
|
||||
delete_contact_and_address,
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.contacts.doctype.address.address import get_default_address
|
||||
from frappe.contacts.doctype.contact.contact import get_default_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
||||
from frappe.utils.data import DateTimeLikeObject
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
from erpnext.selling.doctype.customer.customer import parse_full_name
|
||||
from erpnext.selling.doctype.customer.mapper import parse_full_name
|
||||
|
||||
|
||||
class Lead(SellingController, CRMNote):
|
||||
@@ -322,134 +318,6 @@ class Lead(SellingController, CRMNote):
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name: str, target_doc: str | Document | None = None):
|
||||
return _make_customer(source_name, target_doc)
|
||||
|
||||
|
||||
def _make_customer(source_name, target_doc=None, ignore_permissions=False):
|
||||
def set_missing_values(source, target):
|
||||
if source.company_name:
|
||||
target.customer_type = "Company"
|
||||
target.customer_name = source.company_name
|
||||
else:
|
||||
target.customer_type = "Individual"
|
||||
target.customer_name = source.lead_name
|
||||
|
||||
if not target.customer_group:
|
||||
target.customer_group = frappe.db.get_default("Customer Group")
|
||||
|
||||
address = get_default_address("Lead", source.name)
|
||||
contact = get_default_contact("Lead", source.name)
|
||||
if address:
|
||||
target.customer_primary_address = address
|
||||
if contact:
|
||||
target.customer_primary_contact = contact
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Lead",
|
||||
source_name,
|
||||
{
|
||||
"Lead": {
|
||||
"doctype": "Customer",
|
||||
"field_map": {
|
||||
"name": "lead_name",
|
||||
"company_name": "customer_name",
|
||||
"contact_no": "phone_1",
|
||||
"fax": "fax_1",
|
||||
},
|
||||
"field_no_map": ["disabled"],
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_opportunity(source_name: str, target_doc: str | Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
_set_missing_values(source, target)
|
||||
|
||||
target_doc = get_mapped_doc(
|
||||
"Lead",
|
||||
source_name,
|
||||
{
|
||||
"Lead": {
|
||||
"doctype": "Opportunity",
|
||||
"field_map": {
|
||||
"doctype": "opportunity_from",
|
||||
"name": "party_name",
|
||||
"lead_name": "contact_display",
|
||||
"company_name": "customer_name",
|
||||
"email_id": "contact_email",
|
||||
"mobile_no": "contact_mobile",
|
||||
"lead_owner": "opportunity_owner",
|
||||
"notes": "notes",
|
||||
},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
_set_missing_values(source, target)
|
||||
|
||||
target_doc = get_mapped_doc(
|
||||
"Lead",
|
||||
source_name,
|
||||
{"Lead": {"doctype": "Quotation", "field_map": {"name": "party_name"}}},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
target_doc.quotation_to = "Lead"
|
||||
target_doc.run_method("set_missing_values")
|
||||
target_doc.run_method("set_other_charges")
|
||||
target_doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
def _set_missing_values(source, target):
|
||||
address = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"link_doctype": source.doctype,
|
||||
"link_name": source.name,
|
||||
"parenttype": "Address",
|
||||
},
|
||||
["parent"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
contact = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"link_doctype": source.doctype,
|
||||
"link_name": source.name,
|
||||
"parenttype": "Contact",
|
||||
},
|
||||
["parent"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if address:
|
||||
target.customer_address = address[0].parent
|
||||
|
||||
if contact:
|
||||
target.contact_person = contact[0].parent
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lead_details(
|
||||
lead: str,
|
||||
@@ -494,35 +362,6 @@ def get_lead_details(
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_lead_from_communication(communication: str, ignore_communication_links: bool = False):
|
||||
"""raise a issue from email"""
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
lead_name = None
|
||||
if doc.sender:
|
||||
lead_name = frappe.db.get_value("Lead", {"email_id": doc.sender})
|
||||
if not lead_name and doc.phone_no:
|
||||
lead_name = frappe.db.get_value("Lead", {"mobile_no": doc.phone_no})
|
||||
if not lead_name:
|
||||
lead = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Lead",
|
||||
"lead_name": doc.sender_full_name,
|
||||
"email_id": doc.sender,
|
||||
"mobile_no": doc.phone_no,
|
||||
}
|
||||
)
|
||||
lead.flags.ignore_mandatory = True
|
||||
lead.flags.ignore_permissions = True
|
||||
lead.insert()
|
||||
|
||||
lead_name = lead.name
|
||||
|
||||
link_communication_to_document(doc, "Lead", lead_name, ignore_communication_links)
|
||||
return lead_name
|
||||
|
||||
|
||||
def get_lead_with_phone_number(number):
|
||||
if not number:
|
||||
return
|
||||
|
||||
169
erpnext/crm/doctype/lead/mapper.py
Normal file
169
erpnext/crm/doctype/lead/mapper.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_default_address
|
||||
from frappe.contacts.doctype.contact.contact import get_default_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name: str, target_doc: str | Document | None = None):
|
||||
return _make_customer(source_name, target_doc)
|
||||
|
||||
|
||||
def _make_customer(
|
||||
source_name: str, target_doc: str | Document | None = None, ignore_permissions: bool = False
|
||||
):
|
||||
def set_missing_values(source, target):
|
||||
if source.company_name:
|
||||
target.customer_type = "Company"
|
||||
target.customer_name = source.company_name
|
||||
else:
|
||||
target.customer_type = "Individual"
|
||||
target.customer_name = source.lead_name
|
||||
|
||||
if not target.customer_group:
|
||||
target.customer_group = frappe.db.get_default("Customer Group")
|
||||
|
||||
address = get_default_address("Lead", source.name)
|
||||
contact = get_default_contact("Lead", source.name)
|
||||
if address:
|
||||
target.customer_primary_address = address
|
||||
if contact:
|
||||
target.customer_primary_contact = contact
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Lead",
|
||||
source_name,
|
||||
{
|
||||
"Lead": {
|
||||
"doctype": "Customer",
|
||||
"field_map": {
|
||||
"name": "lead_name",
|
||||
"company_name": "customer_name",
|
||||
"contact_no": "phone_1",
|
||||
"fax": "fax_1",
|
||||
},
|
||||
"field_no_map": ["disabled"],
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_opportunity(source_name: str, target_doc: str | Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
_set_missing_values(source, target)
|
||||
|
||||
target_doc = get_mapped_doc(
|
||||
"Lead",
|
||||
source_name,
|
||||
{
|
||||
"Lead": {
|
||||
"doctype": "Opportunity",
|
||||
"field_map": {
|
||||
"doctype": "opportunity_from",
|
||||
"name": "party_name",
|
||||
"lead_name": "contact_display",
|
||||
"company_name": "customer_name",
|
||||
"email_id": "contact_email",
|
||||
"mobile_no": "contact_mobile",
|
||||
"lead_owner": "opportunity_owner",
|
||||
"notes": "notes",
|
||||
},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
_set_missing_values(source, target)
|
||||
|
||||
target_doc = get_mapped_doc(
|
||||
"Lead",
|
||||
source_name,
|
||||
{"Lead": {"doctype": "Quotation", "field_map": {"name": "party_name"}}},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
target_doc.quotation_to = "Lead"
|
||||
target_doc.run_method("set_missing_values")
|
||||
target_doc.run_method("set_other_charges")
|
||||
target_doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_lead_from_communication(communication: str, ignore_communication_links: bool = False):
|
||||
"""raise a issue from email"""
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
lead_name = None
|
||||
if doc.sender:
|
||||
lead_name = frappe.db.get_value("Lead", {"email_id": doc.sender})
|
||||
if not lead_name and doc.phone_no:
|
||||
lead_name = frappe.db.get_value("Lead", {"mobile_no": doc.phone_no})
|
||||
if not lead_name:
|
||||
lead = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Lead",
|
||||
"lead_name": doc.sender_full_name,
|
||||
"email_id": doc.sender,
|
||||
"mobile_no": doc.phone_no,
|
||||
}
|
||||
)
|
||||
lead.flags.ignore_mandatory = True
|
||||
lead.flags.ignore_permissions = True
|
||||
lead.insert()
|
||||
|
||||
lead_name = lead.name
|
||||
|
||||
link_communication_to_document(doc, "Lead", lead_name, ignore_communication_links)
|
||||
return lead_name
|
||||
|
||||
|
||||
def _set_missing_values(source, target):
|
||||
address = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"link_doctype": source.doctype,
|
||||
"link_name": source.name,
|
||||
"parenttype": "Address",
|
||||
},
|
||||
["parent"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
contact = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"link_doctype": source.doctype,
|
||||
"link_name": source.name,
|
||||
"parenttype": "Contact",
|
||||
},
|
||||
["parent"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if address:
|
||||
target.customer_address = address[0].parent
|
||||
|
||||
if contact:
|
||||
target.contact_person = contact[0].parent
|
||||
@@ -4,14 +4,14 @@
|
||||
import frappe
|
||||
from frappe.utils import random_string, today
|
||||
|
||||
from erpnext.crm.doctype.lead.lead import make_opportunity
|
||||
from erpnext.crm.doctype.lead.mapper import make_opportunity
|
||||
from erpnext.crm.utils import get_linked_prospect
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestLead(ERPNextTestSuite):
|
||||
def test_make_customer(self):
|
||||
from erpnext.crm.doctype.lead.lead import make_customer
|
||||
from erpnext.crm.doctype.lead.mapper import make_customer
|
||||
|
||||
lead = frappe.db.get_all("Lead", {"lead_name": "_Test Lead"})[0].name
|
||||
|
||||
@@ -41,7 +41,7 @@ class TestLead(ERPNextTestSuite):
|
||||
self.assertEqual(contact_doc.has_link(customer.doctype, customer.name), True)
|
||||
|
||||
def test_make_customer_from_organization(self):
|
||||
from erpnext.crm.doctype.lead.lead import make_customer
|
||||
from erpnext.crm.doctype.lead.mapper import make_customer
|
||||
|
||||
lead = frappe.db.get_all("Lead", {"lead_name": "_Test Lead 1"})[0].name
|
||||
customer = make_customer(lead)
|
||||
|
||||
152
erpnext/crm/doctype/opportunity/mapper.py
Normal file
152
erpnext/crm/doctype/opportunity/mapper.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
from erpnext.controllers.accounts_controller import get_default_taxes_and_charges
|
||||
|
||||
quotation = frappe.get_doc(target)
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", quotation.company, "default_currency")
|
||||
|
||||
if company_currency == quotation.currency:
|
||||
exchange_rate = 1
|
||||
else:
|
||||
exchange_rate = get_exchange_rate(
|
||||
quotation.currency, company_currency, quotation.transaction_date, args="for_selling"
|
||||
)
|
||||
|
||||
quotation.conversion_rate = exchange_rate
|
||||
|
||||
# get default taxes
|
||||
taxes = get_default_taxes_and_charges("Sales Taxes and Charges Template", company=quotation.company)
|
||||
if taxes.get("taxes"):
|
||||
quotation.update(taxes)
|
||||
|
||||
quotation.run_method("set_missing_values")
|
||||
quotation.run_method("calculate_taxes_and_totals")
|
||||
if not source.get("items", []):
|
||||
quotation.opportunity = source.name
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Opportunity",
|
||||
source_name,
|
||||
{
|
||||
"Opportunity": {
|
||||
"doctype": "Quotation",
|
||||
"field_map": {"opportunity_from": "quotation_to", "name": "enq_no"},
|
||||
},
|
||||
"Opportunity Item": {
|
||||
"doctype": "Quotation Item",
|
||||
"field_map": {
|
||||
"parent": "prevdoc_docname",
|
||||
"parenttype": "prevdoc_doctype",
|
||||
"uom": "stock_uom",
|
||||
},
|
||||
"add_if_empty": True,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_request_for_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
def update_item(obj, target, source_parent):
|
||||
target.conversion_factor = 1.0
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Opportunity",
|
||||
source_name,
|
||||
{
|
||||
"Opportunity": {"doctype": "Request for Quotation"},
|
||||
"Opportunity Item": {
|
||||
"doctype": "Request for Quotation Item",
|
||||
"field_map": [["name", "opportunity_item"], ["parent", "opportunity"], ["uom", "uom"]],
|
||||
"postprocess": update_item,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name: str, target_doc: str | Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
target.opportunity_name = source.name
|
||||
|
||||
if source.opportunity_from == "Lead":
|
||||
target.lead_name = source.party_name
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Opportunity",
|
||||
source_name,
|
||||
{
|
||||
"Opportunity": {
|
||||
"doctype": "Customer",
|
||||
"field_map": {"currency": "default_currency", "customer_name": "customer_name"},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_supplier_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
doclist = get_mapped_doc(
|
||||
"Opportunity",
|
||||
source_name,
|
||||
{
|
||||
"Opportunity": {"doctype": "Supplier Quotation", "field_map": {"name": "opportunity"}},
|
||||
"Opportunity Item": {"doctype": "Supplier Quotation Item", "field_map": {"uom": "stock_uom"}},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_opportunity_from_communication(
|
||||
communication: str, company: str, ignore_communication_links: bool = False
|
||||
):
|
||||
from erpnext.crm.doctype.lead.mapper import make_lead_from_communication
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
|
||||
lead = doc.reference_name if doc.reference_doctype == "Lead" else None
|
||||
if not lead:
|
||||
lead = make_lead_from_communication(communication, ignore_communication_links=True)
|
||||
|
||||
opportunity_from = "Lead"
|
||||
|
||||
opportunity = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Opportunity",
|
||||
"company": company,
|
||||
"opportunity_from": opportunity_from,
|
||||
"party_name": lead,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||
|
||||
return opportunity.name
|
||||
@@ -40,7 +40,7 @@ frappe.ui.form.on("Opportunity", {
|
||||
erpnext.utils.get_party_details(frm);
|
||||
} else if (frm.doc.opportunity_from == "Lead") {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
method: "erpnext.crm.doctype.lead.mapper.make_opportunity",
|
||||
source_name: frm.doc.party_name,
|
||||
frm: frm,
|
||||
});
|
||||
@@ -204,14 +204,14 @@ frappe.ui.form.on("Opportunity", {
|
||||
|
||||
make_supplier_quotation: function (frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.opportunity.opportunity.make_supplier_quotation",
|
||||
method: "erpnext.crm.doctype.opportunity.mapper.make_supplier_quotation",
|
||||
frm: frm,
|
||||
});
|
||||
},
|
||||
|
||||
make_request_for_quotation: function (frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.opportunity.opportunity.make_request_for_quotation",
|
||||
method: "erpnext.crm.doctype.opportunity.mapper.make_request_for_quotation",
|
||||
frm: frm,
|
||||
});
|
||||
},
|
||||
@@ -341,14 +341,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
|
||||
create_quotation() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.opportunity.opportunity.make_quotation",
|
||||
method: "erpnext.crm.doctype.opportunity.mapper.make_quotation",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_customer() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.opportunity.opportunity.make_customer",
|
||||
method: "erpnext.crm.doctype.opportunity.mapper.make_customer",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
from frappe.utils import flt, get_fullname
|
||||
@@ -389,120 +387,6 @@ def get_item_details(item_code: str):
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
from erpnext.controllers.accounts_controller import get_default_taxes_and_charges
|
||||
|
||||
quotation = frappe.get_doc(target)
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", quotation.company, "default_currency")
|
||||
|
||||
if company_currency == quotation.currency:
|
||||
exchange_rate = 1
|
||||
else:
|
||||
exchange_rate = get_exchange_rate(
|
||||
quotation.currency, company_currency, quotation.transaction_date, args="for_selling"
|
||||
)
|
||||
|
||||
quotation.conversion_rate = exchange_rate
|
||||
|
||||
# get default taxes
|
||||
taxes = get_default_taxes_and_charges("Sales Taxes and Charges Template", company=quotation.company)
|
||||
if taxes.get("taxes"):
|
||||
quotation.update(taxes)
|
||||
|
||||
quotation.run_method("set_missing_values")
|
||||
quotation.run_method("calculate_taxes_and_totals")
|
||||
if not source.get("items", []):
|
||||
quotation.opportunity = source.name
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Opportunity",
|
||||
source_name,
|
||||
{
|
||||
"Opportunity": {
|
||||
"doctype": "Quotation",
|
||||
"field_map": {"opportunity_from": "quotation_to", "name": "enq_no"},
|
||||
},
|
||||
"Opportunity Item": {
|
||||
"doctype": "Quotation Item",
|
||||
"field_map": {
|
||||
"parent": "prevdoc_docname",
|
||||
"parenttype": "prevdoc_doctype",
|
||||
"uom": "stock_uom",
|
||||
},
|
||||
"add_if_empty": True,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_request_for_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
def update_item(obj, target, source_parent):
|
||||
target.conversion_factor = 1.0
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Opportunity",
|
||||
source_name,
|
||||
{
|
||||
"Opportunity": {"doctype": "Request for Quotation"},
|
||||
"Opportunity Item": {
|
||||
"doctype": "Request for Quotation Item",
|
||||
"field_map": [["name", "opportunity_item"], ["parent", "opportunity"], ["uom", "uom"]],
|
||||
"postprocess": update_item,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name: str, target_doc: str | Document | None = None):
|
||||
def set_missing_values(source, target):
|
||||
target.opportunity_name = source.name
|
||||
|
||||
if source.opportunity_from == "Lead":
|
||||
target.lead_name = source.party_name
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Opportunity",
|
||||
source_name,
|
||||
{
|
||||
"Opportunity": {
|
||||
"doctype": "Customer",
|
||||
"field_map": {"currency": "default_currency", "customer_name": "customer_name"},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_supplier_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
doclist = get_mapped_doc(
|
||||
"Opportunity",
|
||||
source_name,
|
||||
{
|
||||
"Opportunity": {"doctype": "Supplier Quotation", "field_map": {"name": "opportunity"}},
|
||||
"Opportunity Item": {"doctype": "Supplier Quotation Item", "field_map": {"uom": "stock_uom"}},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_multiple_status(names: str | list[str], status: str):
|
||||
names = json.loads(names)
|
||||
@@ -531,31 +415,3 @@ def auto_close_opportunity():
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_opportunity_from_communication(
|
||||
communication: str, company: str, ignore_communication_links: bool = False
|
||||
):
|
||||
from erpnext.crm.doctype.lead.lead import make_lead_from_communication
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
|
||||
lead = doc.reference_name if doc.reference_doctype == "Lead" else None
|
||||
if not lead:
|
||||
lead = make_lead_from_communication(communication, ignore_communication_links=True)
|
||||
|
||||
opportunity_from = "Lead"
|
||||
|
||||
opportunity = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Opportunity",
|
||||
"company": company,
|
||||
"opportunity_from": opportunity_from,
|
||||
"party_name": lead,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||
|
||||
return opportunity.name
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, random_string, today
|
||||
|
||||
from erpnext.crm.doctype.lead.lead import make_customer
|
||||
from erpnext.crm.doctype.lead.mapper import make_customer
|
||||
from erpnext.crm.doctype.lead.test_lead import make_lead
|
||||
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
|
||||
from erpnext.crm.doctype.opportunity.mapper import make_quotation
|
||||
from erpnext.crm.utils import get_linked_communication_list
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ erpnext.maintenance.MaintenanceSchedule = class MaintenanceSchedule extends frap
|
||||
__("Sales Order"),
|
||||
function () {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_schedule",
|
||||
method: "erpnext.selling.doctype.sales_order.mapper.make_maintenance_schedule",
|
||||
source_doctype: "Sales Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
|
||||
@@ -126,7 +126,7 @@ erpnext.maintenance.MaintenanceVisit = class MaintenanceVisit extends frappe.ui.
|
||||
return;
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_visit",
|
||||
method: "erpnext.selling.doctype.sales_order.mapper.make_maintenance_visit",
|
||||
source_doctype: "Sales Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
|
||||
@@ -368,7 +368,7 @@ frappe.ui.form.on("Job Card", {
|
||||
if (frm.doc.docstatus === 1 && frm.doc.for_quantity > frm.doc.manufactured_qty) {
|
||||
frm.add_custom_button(__("Make Subcontracting PO"), () => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.manufacturing.doctype.job_card.job_card.make_subcontracting_po",
|
||||
method: "erpnext.manufacturing.doctype.job_card.mapper.make_subcontracting_po",
|
||||
frm: frm,
|
||||
});
|
||||
}).addClass("btn-primary");
|
||||
@@ -483,7 +483,7 @@ frappe.ui.form.on("Job Card", {
|
||||
|
||||
make_corrective_job_card(frm, operation, for_operation) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card",
|
||||
method: "erpnext.manufacturing.doctype.job_card.mapper.make_corrective_job_card",
|
||||
args: {
|
||||
source_name: frm.doc.name,
|
||||
operation: operation,
|
||||
@@ -816,7 +816,7 @@ frappe.ui.form.on("Job Card", {
|
||||
|
||||
make_material_request(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request",
|
||||
method: "erpnext.manufacturing.doctype.job_card.mapper.make_material_request",
|
||||
frm: frm,
|
||||
run_link_triggers: true,
|
||||
});
|
||||
@@ -824,7 +824,7 @@ frappe.ui.form.on("Job Card", {
|
||||
|
||||
make_stock_entry(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry",
|
||||
method: "erpnext.manufacturing.doctype.job_card.mapper.make_stock_entry",
|
||||
frm: frm,
|
||||
run_link_triggers: true,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import IfNull, Max, Min, Sum
|
||||
from frappe.utils import (
|
||||
@@ -37,6 +36,10 @@ from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import
|
||||
get_subcontracting_boms_for_finished_goods,
|
||||
)
|
||||
|
||||
from .mapper import (
|
||||
make_stock_entry,
|
||||
)
|
||||
|
||||
|
||||
class OverlapError(frappe.ValidationError):
|
||||
pass
|
||||
@@ -1547,47 +1550,6 @@ class JobCard(Document):
|
||||
return ste.stock_entry.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_po(source_name: str, target_doc: Document | str | None = None):
|
||||
def set_missing_values(source, target):
|
||||
_item_details = get_subcontracting_boms_for_finished_goods(source.finished_good)
|
||||
|
||||
pending_qty = source.for_quantity - source.manufactured_qty
|
||||
service_item_qty = flt(_item_details.service_item_qty) or 1.0
|
||||
fg_item_qty = flt(_item_details.finished_good_qty) or 1.0
|
||||
|
||||
target.is_subcontracted = 1
|
||||
target.supplier_warehouse = source.wip_warehouse
|
||||
target.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": _item_details.service_item,
|
||||
"fg_item": source.finished_good,
|
||||
"uom": _item_details.service_item_uom,
|
||||
"stock_uom": _item_details.service_item_uom,
|
||||
"conversion_factor": _item_details.conversion_factor or 1,
|
||||
"item_name": _item_details.service_item,
|
||||
"qty": pending_qty * service_item_qty / fg_item_qty,
|
||||
"fg_item_qty": pending_qty,
|
||||
"job_card": source.name,
|
||||
"bom": source.semi_fg_bom,
|
||||
"warehouse": source.target_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
source_name,
|
||||
{
|
||||
"Job Card": {"doctype": "Purchase Order", "field_no_map": ["naming_series"]},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_time_log(kwargs: str | dict):
|
||||
if isinstance(kwargs, str):
|
||||
@@ -1631,105 +1593,6 @@ def get_operations(doctype: str, txt: str, searchfield: str, start: int, page_le
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_material_request(source_name: str, target_doc: Document | str | None = None):
|
||||
def update_item(obj, target, source_parent):
|
||||
target.warehouse = source_parent.wip_warehouse
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.material_request_type = "Material Transfer"
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
source_name,
|
||||
{
|
||||
"Job Card": {
|
||||
"doctype": "Material Request",
|
||||
"field_map": {
|
||||
"name": "job_card",
|
||||
},
|
||||
},
|
||||
"Job Card Item": {
|
||||
"doctype": "Material Request Item",
|
||||
"field_map": {"required_qty": "qty", "uom": "stock_uom", "name": "job_card_item"},
|
||||
"postprocess": update_item,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(source_name: str, target_doc: Document | str | None = None):
|
||||
def update_item(source, target, source_parent):
|
||||
target.t_warehouse = source_parent.wip_warehouse
|
||||
|
||||
if not target.conversion_factor:
|
||||
target.conversion_factor = 1
|
||||
|
||||
pending_rm_qty = flt(source.required_qty) - flt(source.transferred_qty)
|
||||
if pending_rm_qty > 0:
|
||||
target.qty = pending_rm_qty
|
||||
|
||||
def set_missing_values(source, target):
|
||||
if source.finished_good and not source.target_warehouse:
|
||||
frappe.throw(_("Please set the Target Warehouse in the Job Card"))
|
||||
|
||||
if not source.skip_material_transfer or source.backflush_from_wip_warehouse:
|
||||
if not source.wip_warehouse:
|
||||
frappe.throw(_("Please set the WIP Warehouse in the Job Card"))
|
||||
|
||||
target.purpose = "Material Transfer for Manufacture"
|
||||
target.from_bom = 1
|
||||
|
||||
if source.semi_fg_bom:
|
||||
target.bom_no = source.semi_fg_bom
|
||||
|
||||
# avoid negative 'For Quantity'
|
||||
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
|
||||
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
|
||||
|
||||
target.set_missing_values()
|
||||
target.set_stock_entry_type()
|
||||
|
||||
wo_allows_alternate_item = frappe.db.get_value(
|
||||
"Work Order", target.work_order, "allow_alternative_item"
|
||||
)
|
||||
for item in target.items:
|
||||
item.allow_alternative_item = int(
|
||||
wo_allows_alternate_item
|
||||
and frappe.get_cached_value("Item", item.item_code, "allow_alternative_item")
|
||||
)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
source_name,
|
||||
{
|
||||
"Job Card": {
|
||||
"doctype": "Stock Entry",
|
||||
"field_map": {"name": "job_card", "for_quantity": "fg_completed_qty"},
|
||||
},
|
||||
"Job Card Item": {
|
||||
"doctype": "Stock Entry Detail",
|
||||
"field_map": {
|
||||
"source_warehouse": "s_warehouse",
|
||||
"required_qty": "qty",
|
||||
"name": "job_card_item",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.required_qty > 0,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
def time_diff_in_minutes(string_ed_date, string_st_date):
|
||||
return time_diff(string_ed_date, string_st_date).total_seconds() / 60
|
||||
|
||||
@@ -1780,40 +1643,3 @@ def get_job_details(start: Any, end: Any, filters: str | dict | None = None):
|
||||
events.append(job_card_data)
|
||||
|
||||
return events
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_corrective_job_card(
|
||||
source_name: str,
|
||||
operation: str | None = None,
|
||||
for_operation: str | None = None,
|
||||
target_doc: Document | str | None = None,
|
||||
):
|
||||
def set_missing_values(source, target):
|
||||
target.is_corrective_job_card = 1
|
||||
target.operation = operation
|
||||
target.for_operation = for_operation
|
||||
|
||||
target.set("time_logs", [])
|
||||
target.set("employee", [])
|
||||
target.set("items", [])
|
||||
target.set("sub_operations", [])
|
||||
target.set_sub_operations()
|
||||
target.get_required_items()
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
source_name,
|
||||
{
|
||||
"Job Card": {
|
||||
"doctype": "Job Card",
|
||||
"field_map": {
|
||||
"name": "for_job_card",
|
||||
},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
189
erpnext/manufacturing/doctype/job_card/mapper.py
Normal file
189
erpnext/manufacturing/doctype/job_card/mapper.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
|
||||
get_subcontracting_boms_for_finished_goods,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_po(source_name: str, target_doc: Document | str | None = None):
|
||||
def set_missing_values(source, target):
|
||||
_item_details = get_subcontracting_boms_for_finished_goods(source.finished_good)
|
||||
|
||||
pending_qty = source.for_quantity - source.manufactured_qty
|
||||
service_item_qty = flt(_item_details.service_item_qty) or 1.0
|
||||
fg_item_qty = flt(_item_details.finished_good_qty) or 1.0
|
||||
|
||||
target.is_subcontracted = 1
|
||||
target.supplier_warehouse = source.wip_warehouse
|
||||
target.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": _item_details.service_item,
|
||||
"fg_item": source.finished_good,
|
||||
"uom": _item_details.service_item_uom,
|
||||
"stock_uom": _item_details.service_item_uom,
|
||||
"conversion_factor": _item_details.conversion_factor or 1,
|
||||
"item_name": _item_details.service_item,
|
||||
"qty": pending_qty * service_item_qty / fg_item_qty,
|
||||
"fg_item_qty": pending_qty,
|
||||
"job_card": source.name,
|
||||
"bom": source.semi_fg_bom,
|
||||
"warehouse": source.target_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
source_name,
|
||||
{
|
||||
"Job Card": {"doctype": "Purchase Order", "field_no_map": ["naming_series"]},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_material_request(source_name: str, target_doc: Document | str | None = None):
|
||||
def update_item(obj, target, source_parent):
|
||||
target.warehouse = source_parent.wip_warehouse
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.material_request_type = "Material Transfer"
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
source_name,
|
||||
{
|
||||
"Job Card": {
|
||||
"doctype": "Material Request",
|
||||
"field_map": {
|
||||
"name": "job_card",
|
||||
},
|
||||
},
|
||||
"Job Card Item": {
|
||||
"doctype": "Material Request Item",
|
||||
"field_map": {"required_qty": "qty", "uom": "stock_uom", "name": "job_card_item"},
|
||||
"postprocess": update_item,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(source_name: str, target_doc: Document | str | None = None):
|
||||
def update_item(source, target, source_parent):
|
||||
target.t_warehouse = source_parent.wip_warehouse
|
||||
|
||||
if not target.conversion_factor:
|
||||
target.conversion_factor = 1
|
||||
|
||||
pending_rm_qty = flt(source.required_qty) - flt(source.transferred_qty)
|
||||
if pending_rm_qty > 0:
|
||||
target.qty = pending_rm_qty
|
||||
|
||||
def set_missing_values(source, target):
|
||||
if source.finished_good and not source.target_warehouse:
|
||||
frappe.throw(_("Please set the Target Warehouse in the Job Card"))
|
||||
|
||||
if not source.skip_material_transfer or source.backflush_from_wip_warehouse:
|
||||
if not source.wip_warehouse:
|
||||
frappe.throw(_("Please set the WIP Warehouse in the Job Card"))
|
||||
|
||||
target.purpose = "Material Transfer for Manufacture"
|
||||
target.from_bom = 1
|
||||
|
||||
if source.semi_fg_bom:
|
||||
target.bom_no = source.semi_fg_bom
|
||||
|
||||
# avoid negative 'For Quantity'
|
||||
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
|
||||
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
|
||||
|
||||
target.set_missing_values()
|
||||
target.set_stock_entry_type()
|
||||
|
||||
wo_allows_alternate_item = frappe.db.get_value(
|
||||
"Work Order", target.work_order, "allow_alternative_item"
|
||||
)
|
||||
for item in target.items:
|
||||
item.allow_alternative_item = int(
|
||||
wo_allows_alternate_item
|
||||
and frappe.get_cached_value("Item", item.item_code, "allow_alternative_item")
|
||||
)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
source_name,
|
||||
{
|
||||
"Job Card": {
|
||||
"doctype": "Stock Entry",
|
||||
"field_map": {"name": "job_card", "for_quantity": "fg_completed_qty"},
|
||||
},
|
||||
"Job Card Item": {
|
||||
"doctype": "Stock Entry Detail",
|
||||
"field_map": {
|
||||
"source_warehouse": "s_warehouse",
|
||||
"required_qty": "qty",
|
||||
"name": "job_card_item",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.required_qty > 0,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_corrective_job_card(
|
||||
source_name: str,
|
||||
operation: str | None = None,
|
||||
for_operation: str | None = None,
|
||||
target_doc: Document | str | None = None,
|
||||
):
|
||||
def set_missing_values(source, target):
|
||||
target.is_corrective_job_card = 1
|
||||
target.operation = operation
|
||||
target.for_operation = for_operation
|
||||
|
||||
target.set("time_logs", [])
|
||||
target.set("employee", [])
|
||||
target.set("items", [])
|
||||
target.set("sub_operations", [])
|
||||
target.set_sub_operations()
|
||||
target.get_required_items()
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
source_name,
|
||||
{
|
||||
"Job Card": {
|
||||
"doctype": "Job Card",
|
||||
"field_map": {
|
||||
"name": "for_job_card",
|
||||
},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
return doclist
|
||||
@@ -12,10 +12,12 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
JobCardOverTransferError,
|
||||
OperationMismatchError,
|
||||
OverlapError,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.job_card.mapper import (
|
||||
make_corrective_job_card,
|
||||
make_material_request,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
from erpnext.manufacturing.doctype.job_card.mapper import (
|
||||
make_stock_entry as make_stock_entry_from_jc,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
@@ -552,7 +554,7 @@ class TestJobCard(ERPNextTestSuite):
|
||||
corrective_job_card.submit()
|
||||
wo.reload()
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
|
||||
@@ -623,7 +625,7 @@ class TestJobCard(ERPNextTestSuite):
|
||||
assertStatus("Cancelled")
|
||||
|
||||
def test_job_card_material_request_and_bom_details(self):
|
||||
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
|
||||
from erpnext.stock.doctype.material_request.mapper import make_stock_entry
|
||||
|
||||
create_bom_with_multiple_operations()
|
||||
work_order = make_wo_with_transfer_against_jc()
|
||||
@@ -647,7 +649,7 @@ class TestJobCard(ERPNextTestSuite):
|
||||
setup_bom,
|
||||
setup_operations,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -788,10 +790,10 @@ class TestJobCard(ERPNextTestSuite):
|
||||
setup_bom,
|
||||
setup_operations,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
@@ -1075,7 +1077,7 @@ class TestJobCard(ERPNextTestSuite):
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
|
||||
@@ -1094,10 +1096,10 @@ class TestJobCard(ERPNextTestSuite):
|
||||
setup_bom,
|
||||
setup_operations,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_sales_orders,
|
||||
get_warehouse_list,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry as make_se_from_wo
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_se_from_wo
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
@@ -523,13 +523,13 @@ class TestProductionPlan(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
def make_purchase_receipt_from_po(po_doc):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_subcontracting_order
|
||||
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
|
||||
from erpnext.subcontracting.doctype.subcontracting_receipt.mapper import (
|
||||
make_purchase_receipt as scr_make_purchase_receipt,
|
||||
)
|
||||
|
||||
@@ -2211,9 +2211,9 @@ class TestProductionPlan(ERPNextTestSuite):
|
||||
self.assertEqual(mr_items_dict["RM Item 2"], 80)
|
||||
|
||||
def test_stock_reservation_against_production_plan(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.mapper import make_purchase_order
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
|
||||
|
||||
@@ -2323,9 +2323,9 @@ class TestProductionPlan(ERPNextTestSuite):
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
|
||||
|
||||
def test_stock_reservation_of_serial_nos_against_production_plan(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.mapper import make_purchase_order
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
|
||||
|
||||
@@ -2470,9 +2470,9 @@ class TestProductionPlan(ERPNextTestSuite):
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
|
||||
|
||||
def test_stock_reservation_of_batch_nos_against_production_plan(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.mapper import make_purchase_order
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
|
||||
|
||||
|
||||
134
erpnext/manufacturing/doctype/work_order/mapper.py
Normal file
134
erpnext/manufacturing/doctype/work_order/mapper.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(
|
||||
work_order_id: str,
|
||||
purpose: str,
|
||||
qty: float | None = None,
|
||||
target_warehouse: str | None = None,
|
||||
is_additional_transfer_entry: bool = False,
|
||||
source_stock_entry: str | None = None,
|
||||
):
|
||||
work_order = frappe.get_doc("Work Order", work_order_id)
|
||||
if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
|
||||
wip_warehouse = work_order.wip_warehouse
|
||||
else:
|
||||
wip_warehouse = None
|
||||
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.purpose = purpose
|
||||
stock_entry.work_order = work_order_id
|
||||
stock_entry.company = work_order.company
|
||||
stock_entry.from_bom = 1
|
||||
stock_entry.bom_no = work_order.bom_no
|
||||
stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
|
||||
if purpose in ["Material Transfer for Manufacture", "Manufacture"]:
|
||||
stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order
|
||||
# accept 0 qty as well
|
||||
stock_entry.fg_completed_qty = (
|
||||
qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty))
|
||||
)
|
||||
|
||||
if purpose == "Material Transfer for Manufacture":
|
||||
stock_entry.to_warehouse = wip_warehouse
|
||||
stock_entry.project = work_order.project
|
||||
else:
|
||||
stock_entry.from_warehouse = (
|
||||
work_order.source_warehouse
|
||||
if work_order.skip_transfer and not work_order.from_wip_warehouse
|
||||
else wip_warehouse
|
||||
)
|
||||
stock_entry.to_warehouse = work_order.fg_warehouse
|
||||
stock_entry.project = work_order.project
|
||||
if work_order.bom_no:
|
||||
stock_entry.inspection_required = frappe.db.get_value(
|
||||
"BOM", work_order.bom_no, "inspection_required"
|
||||
)
|
||||
|
||||
if purpose == "Disassemble":
|
||||
stock_entry.from_warehouse = work_order.fg_warehouse
|
||||
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
|
||||
if source_stock_entry:
|
||||
stock_entry.source_stock_entry = source_stock_entry
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.is_additional_transfer_entry = is_additional_transfer_entry
|
||||
stock_entry.get_items()
|
||||
|
||||
return stock_entry.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_pick_list(source_name: str, target_doc: str | None = None, for_qty: float | None = None):
|
||||
for_qty = for_qty or json.loads(target_doc).get("for_qty")
|
||||
max_finished_goods_qty = frappe.db.get_value("Work Order", source_name, "qty")
|
||||
|
||||
def update_item_quantity(source, target, source_parent):
|
||||
pending_to_issue = flt(source.required_qty) - flt(source.transferred_qty)
|
||||
desire_to_transfer = flt(source.required_qty) / max_finished_goods_qty * flt(for_qty)
|
||||
|
||||
qty = 0
|
||||
if desire_to_transfer <= pending_to_issue:
|
||||
qty = desire_to_transfer
|
||||
elif pending_to_issue > 0:
|
||||
qty = pending_to_issue
|
||||
|
||||
if qty:
|
||||
target.qty = qty
|
||||
target.stock_qty = qty
|
||||
target.uom = frappe.get_value("Item", source.item_code, "stock_uom")
|
||||
target.stock_uom = target.uom
|
||||
target.conversion_factor = 1
|
||||
else:
|
||||
target.delete()
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Work Order",
|
||||
source_name,
|
||||
{
|
||||
"Work Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}},
|
||||
"Work Order Item": {
|
||||
"doctype": "Pick List Item",
|
||||
"postprocess": update_item_quantity,
|
||||
"condition": lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty),
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
doc.purpose = "Material Transfer for Manufacture"
|
||||
doc.for_qty = for_qty
|
||||
|
||||
doc.set_item_locations()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_return_entry(work_order: str):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_handler.manufacturing import (
|
||||
ManufactureStockEntry,
|
||||
)
|
||||
|
||||
wo_doc = frappe.get_cached_doc("Work Order", work_order)
|
||||
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.from_bom = 1
|
||||
stock_entry.is_return = 1
|
||||
stock_entry.work_order = work_order
|
||||
stock_entry.purpose = "Material Transfer for Manufacture"
|
||||
stock_entry.bom_no = wo_doc.bom_no
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
ste_cls = ManufactureStockEntry(stock_entry)
|
||||
ste_cls.add_raw_materials_based_on_transfer()
|
||||
ste_cls.return_available_materials_in_source_wh()
|
||||
return stock_entry
|
||||
@@ -9,8 +9,12 @@ from frappe.tests import timeout
|
||||
from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, nowdate, nowtime, today
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import make_stock_entry as make_stock_entry_from_jc
|
||||
from erpnext.manufacturing.doctype.job_card.mapper import make_stock_entry as make_stock_entry_from_jc
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import (
|
||||
make_stock_entry,
|
||||
make_stock_return_entry,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
CapacityError,
|
||||
ItemHasVariantError,
|
||||
@@ -18,8 +22,6 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
StockOverProductionError,
|
||||
close_work_order,
|
||||
make_job_card,
|
||||
make_stock_entry,
|
||||
make_stock_return_entry,
|
||||
stop_unstop,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
@@ -289,7 +289,7 @@ frappe.ui.form.on("Work Order", {
|
||||
|
||||
create_stock_return_entry: function (frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.make_stock_return_entry",
|
||||
method: "erpnext.manufacturing.doctype.work_order.mapper.make_stock_return_entry",
|
||||
args: {
|
||||
work_order: frm.doc.name,
|
||||
},
|
||||
@@ -445,7 +445,7 @@ frappe.ui.form.on("Work Order", {
|
||||
frappe.msgprint(__("Disassemble Qty cannot be less than or equal to <b>0</b>."));
|
||||
return;
|
||||
}
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.mapper.make_stock_entry", {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: "Disassemble",
|
||||
qty: data.qty,
|
||||
@@ -822,7 +822,7 @@ erpnext.work_order = {
|
||||
.show_prompt_for_qty_input(frm, purpose, qty, 1)
|
||||
.then((data) => {
|
||||
return frappe.xcall(
|
||||
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
|
||||
"erpnext.manufacturing.doctype.work_order.mapper.make_stock_entry",
|
||||
{
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: purpose,
|
||||
@@ -1110,7 +1110,7 @@ erpnext.work_order = {
|
||||
make_se: function (frm, purpose, qty, is_additional_transfer_entry) {
|
||||
if (qty) {
|
||||
frappe
|
||||
.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
||||
.xcall("erpnext.manufacturing.doctype.work_order.mapper.make_stock_entry", {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: purpose,
|
||||
qty: qty,
|
||||
@@ -1123,14 +1123,11 @@ erpnext.work_order = {
|
||||
} else {
|
||||
this.show_prompt_for_qty_input(frm, purpose)
|
||||
.then((data) => {
|
||||
return frappe.xcall(
|
||||
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
|
||||
{
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: purpose,
|
||||
qty: data.qty,
|
||||
}
|
||||
);
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.mapper.make_stock_entry", {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: purpose,
|
||||
qty: data.qty,
|
||||
});
|
||||
})
|
||||
.then((stock_entry) => {
|
||||
frappe.model.sync(stock_entry);
|
||||
@@ -1142,7 +1139,7 @@ erpnext.work_order = {
|
||||
create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") {
|
||||
this.show_prompt_for_qty_input(frm, purpose)
|
||||
.then((data) => {
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.create_pick_list", {
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.mapper.create_pick_list", {
|
||||
source_name: frm.doc.name,
|
||||
for_qty: data.qty,
|
||||
});
|
||||
@@ -1166,7 +1163,7 @@ erpnext.work_order = {
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
|
||||
method: "erpnext.manufacturing.doctype.work_order.mapper.make_stock_entry",
|
||||
args: {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: "Material Consumption for Manufacture",
|
||||
|
||||
@@ -8,7 +8,6 @@ import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import (
|
||||
@@ -2400,64 +2399,6 @@ def set_work_order_ops(name: str):
|
||||
po.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(
|
||||
work_order_id: str,
|
||||
purpose: str,
|
||||
qty: float | None = None,
|
||||
target_warehouse: str | None = None,
|
||||
is_additional_transfer_entry: bool = False,
|
||||
source_stock_entry: str | None = None,
|
||||
):
|
||||
work_order = frappe.get_doc("Work Order", work_order_id)
|
||||
if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
|
||||
wip_warehouse = work_order.wip_warehouse
|
||||
else:
|
||||
wip_warehouse = None
|
||||
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.purpose = purpose
|
||||
stock_entry.work_order = work_order_id
|
||||
stock_entry.company = work_order.company
|
||||
stock_entry.from_bom = 1
|
||||
stock_entry.bom_no = work_order.bom_no
|
||||
stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
|
||||
if purpose in ["Material Transfer for Manufacture", "Manufacture"]:
|
||||
stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order
|
||||
# accept 0 qty as well
|
||||
stock_entry.fg_completed_qty = (
|
||||
qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty))
|
||||
)
|
||||
|
||||
if purpose == "Material Transfer for Manufacture":
|
||||
stock_entry.to_warehouse = wip_warehouse
|
||||
stock_entry.project = work_order.project
|
||||
else:
|
||||
stock_entry.from_warehouse = (
|
||||
work_order.source_warehouse
|
||||
if work_order.skip_transfer and not work_order.from_wip_warehouse
|
||||
else wip_warehouse
|
||||
)
|
||||
stock_entry.to_warehouse = work_order.fg_warehouse
|
||||
stock_entry.project = work_order.project
|
||||
if work_order.bom_no:
|
||||
stock_entry.inspection_required = frappe.db.get_value(
|
||||
"BOM", work_order.bom_no, "inspection_required"
|
||||
)
|
||||
|
||||
if purpose == "Disassemble":
|
||||
stock_entry.from_warehouse = work_order.fg_warehouse
|
||||
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
|
||||
if source_stock_entry:
|
||||
stock_entry.source_stock_entry = source_stock_entry
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.is_additional_transfer_entry = is_additional_transfer_entry
|
||||
stock_entry.get_items()
|
||||
|
||||
return stock_entry.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_disassembly_available_qty(stock_entry_name: str, current_se_name: str | None = None) -> float:
|
||||
se = frappe.db.get_value("Stock Entry", stock_entry_name, ["fg_completed_qty"], as_dict=True)
|
||||
@@ -2717,52 +2658,6 @@ def get_work_order_operation_data(work_order, operation, workstation):
|
||||
return d
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_pick_list(source_name: str, target_doc: str | None = None, for_qty: float | None = None):
|
||||
for_qty = for_qty or json.loads(target_doc).get("for_qty")
|
||||
max_finished_goods_qty = frappe.db.get_value("Work Order", source_name, "qty")
|
||||
|
||||
def update_item_quantity(source, target, source_parent):
|
||||
pending_to_issue = flt(source.required_qty) - flt(source.transferred_qty)
|
||||
desire_to_transfer = flt(source.required_qty) / max_finished_goods_qty * flt(for_qty)
|
||||
|
||||
qty = 0
|
||||
if desire_to_transfer <= pending_to_issue:
|
||||
qty = desire_to_transfer
|
||||
elif pending_to_issue > 0:
|
||||
qty = pending_to_issue
|
||||
|
||||
if qty:
|
||||
target.qty = qty
|
||||
target.stock_qty = qty
|
||||
target.uom = frappe.get_value("Item", source.item_code, "stock_uom")
|
||||
target.stock_uom = target.uom
|
||||
target.conversion_factor = 1
|
||||
else:
|
||||
target.delete()
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Work Order",
|
||||
source_name,
|
||||
{
|
||||
"Work Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}},
|
||||
"Work Order Item": {
|
||||
"doctype": "Pick List Item",
|
||||
"postprocess": update_item_quantity,
|
||||
"condition": lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty),
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
doc.purpose = "Material Transfer for Manufacture"
|
||||
doc.for_qty = for_qty
|
||||
|
||||
doc.set_item_locations()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_reserved_qty_for_production(
|
||||
item_code: str,
|
||||
warehouse: str,
|
||||
@@ -2812,28 +2707,6 @@ def get_reserved_qty_for_production(
|
||||
return query.run()[0][0] or 0.0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_return_entry(work_order: str):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_handler.manufacturing import (
|
||||
ManufactureStockEntry,
|
||||
)
|
||||
|
||||
wo_doc = frappe.get_cached_doc("Work Order", work_order)
|
||||
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.from_bom = 1
|
||||
stock_entry.is_return = 1
|
||||
stock_entry.work_order = work_order
|
||||
stock_entry.purpose = "Material Transfer for Manufacture"
|
||||
stock_entry.bom_no = wo_doc.bom_no
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
ste_cls = ManufactureStockEntry(stock_entry)
|
||||
ste_cls.add_raw_materials_based_on_transfer()
|
||||
ste_cls.return_available_materials_in_source_wh()
|
||||
return stock_entry
|
||||
|
||||
|
||||
def get_row_wise_serial_batch(work_order, purpose=None):
|
||||
if not purpose:
|
||||
purpose = "Material Transfer for Manufacture"
|
||||
|
||||
@@ -402,7 +402,7 @@ class WorkstationDashboard {
|
||||
if (r.message) {
|
||||
me.prepare_materials_modal(r.message, job_card, (job_card) => {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry",
|
||||
method: "erpnext.manufacturing.doctype.job_card.mapper.make_stock_entry",
|
||||
args: {
|
||||
source_name: job_card,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ from frappe.utils import add_days, getdate, nowdate
|
||||
|
||||
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
|
||||
from erpnext.projects.doctype.task.test_task import create_task
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_project as make_project_from_so
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user