Merge branch 'develop' into internal_trasfer_precision_loss

This commit is contained in:
Deepesh Garg 2022-06-21 10:45:02 +05:30 committed by GitHub
commit f572c20009
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
180 changed files with 3190 additions and 1843 deletions

View File

@ -115,4 +115,5 @@ jobs:
echo "Updating to latest version"
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
bench setup requirements --python
bench --site test_site migrate

View File

@ -9,6 +9,8 @@ pull_request_rules:
- author!=nabinhait
- author!=ankush
- author!=deepeshgarg007
- author!=mergify[bot]
- or:
- base=version-13
- base=version-12
@ -19,6 +21,16 @@ pull_request_rules:
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
- name: Auto-close PRs on pre-release branch
conditions:
- base=version-13-pre-release
actions:
close:
comment:
message: |
@{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches.
- name: backport to develop
conditions:
- label="backport develop"

View File

@ -10,4 +10,42 @@ Entries are:
- Sales Invoice (Itemised)
- Purchase Invoice (Itemised)
All accounting entries are stored in the `General Ledger`
All accounting entries are stored in the `General Ledger`
## Payment Ledger
Transactions on Receivable and Payable Account types will also be stored in `Payment Ledger`. This is so that payment reconciliation process only requires update on this ledger.
### Key Fields
| Field | Description |
|----------------------|----------------------------------|
| `account_type` | Receivable/Payable |
| `account` | Accounting head |
| `party` | Party Name |
| `voucher_no` | Voucher No |
| `against_voucher_no` | Linked voucher(secondary effect) |
| `amount` | can be +ve/-ve |
### Design
`debit` and `credit` have been replaced with `account_type` and `amount`. `against_voucher_no` is populated for all entries. So, outstanding amount can be calculated by summing up amount only using `against_voucher_no`.
Ex:
1. Consider an invoice for ₹100 and a partial payment of ₹80 against that invoice. Payment Ledger will have following entries.
| voucher_no | against_voucher_no | amount |
|------------|--------------------|--------|
| SINV-01 | SINV-01 | 100 |
| PAY-01 | SINV-01 | -80 |
2. Reconcile a Credit Note against an invoice using a Journal Entry
An invoice for ₹100 partially reconciled against a credit of ₹70 using a Journal Entry. Payment Ledger will have the following entries.
| voucher_no | against_voucher_no | amount |
|------------|--------------------|--------|
| SINV-01 | SINV-01 | 100 |
| | | |
| CR-NOTE-01 | CR-NOTE-01 | -70 |
| | | |
| JE-01 | CR-NOTE-01 | +70 |
| JE-01 | SINV-01 | -70 |

View File

@ -322,9 +322,9 @@ def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
"""select name from tabAccount
where is_group = 1 and docstatus != 2 and company = %s
and %s like %s order by name limit %s, %s"""
and %s like %s order by name limit %s offset %s"""
% ("%s", searchfield, "%s", "%s", "%s"),
(filters["company"], "%%%s%%" % txt, start, page_len),
(filters["company"], "%%%s%%" % txt, page_len, start),
as_list=1,
)

View File

@ -58,16 +58,20 @@ class GLEntry(Document):
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
# Update outstanding amt on against voucher
if (
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
and self.against_voucher
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):
update_outstanding_amt(
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
)
if frappe.db.get_value("Account", self.account, "account_type") not in [
"Receivable",
"Payable",
]:
# Update outstanding amt on against voucher
if (
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
and self.against_voucher
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):
update_outstanding_amt(
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
)
def check_mandatory(self):
mandatory = ["account", "voucher_type", "voucher_no", "company"]

View File

@ -416,7 +416,7 @@ class JournalEntry(AccountsController):
against_entries = frappe.db.sql(
"""select * from `tabJournal Entry Account`
where account = %s and docstatus = 1 and parent = %s
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
and (reference_type is null or reference_type in ('', 'Sales Order', 'Purchase Order'))
""",
(d.account, d.reference_name),
as_dict=True,
@ -800,9 +800,7 @@ class JournalEntry(AccountsController):
self.total_amount_in_words = money_in_words(amt, currency)
def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries
def build_gl_map(self):
gl_map = []
for d in self.get("accounts"):
if d.debit or d.credit:
@ -838,7 +836,12 @@ class JournalEntry(AccountsController):
item=d,
)
)
return gl_map
def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries
gl_map = self.build_gl_map()
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
update_outstanding = "No"
else:
@ -1239,7 +1242,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
AND jv.docstatus = 1
AND jv.`{0}` LIKE %(txt)s
ORDER BY jv.name DESC
LIMIT %(offset)s, %(limit)s
LIMIT %(limit)s offset %(offset)s
""".format(
searchfield
),

View File

@ -6,7 +6,7 @@ import json
from functools import reduce
import frappe
from frappe import ValidationError, _, scrub, throw
from frappe import ValidationError, _, qb, scrub, throw
from frappe.utils import cint, comma_or, flt, getdate, nowdate
import erpnext
@ -785,7 +785,7 @@ class PaymentEntry(AccountsController):
self.set("remarks", "\n".join(remarks))
def make_gl_entries(self, cancel=0, adv_adj=0):
def build_gl_map(self):
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
self.setup_party_account_field()
@ -794,7 +794,10 @@ class PaymentEntry(AccountsController):
self.add_bank_gl_entries(gl_entries)
self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries)
return gl_entries
def make_gl_entries(self, cancel=0, adv_adj=0):
gl_entries = self.build_gl_map()
gl_entries = process_gl_map(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
@ -1195,6 +1198,9 @@ def get_outstanding_reference_documents(args):
if args.get("party_type") == "Member":
return
ple = qb.DocType("Payment Ledger Entry")
common_filter = []
# confirm that Supplier is not blocked
if args.get("party_type") == "Supplier":
supplier_status = get_supplier_block_status(args["party"])
@ -1216,10 +1222,13 @@ def get_outstanding_reference_documents(args):
condition = " and voucher_type={0} and voucher_no={1}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
)
common_filter.append(ple.voucher_type == args["voucher_type"])
common_filter.append(ple.voucher_no == args["voucher_no"])
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
common_filter.append(ple.cost_center == args.get("cost_center"))
date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"],
@ -1231,16 +1240,19 @@ def get_outstanding_reference_documents(args):
condition += " and {0} between '{1}' and '{2}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
common_filter.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
common_filter.append(ple.company == args.get("company"))
outstanding_invoices = get_outstanding_invoices(
args.get("party_type"),
args.get("party"),
args.get("party_account"),
filters=args,
condition=condition,
common_filter=common_filter,
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
)
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@ -1444,7 +1456,7 @@ def get_negative_outstanding_invoices(
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = ""
if voucher_type == "Purchase Invoice":
supplier_condition = "and (release_date is null or release_date <= CURDATE())"
supplier_condition = "and (release_date is null or release_date <= CURRENT_DATE)"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total"

View File

@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
@ -24,7 +25,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
test_dependencies = ["Item"]
class TestPaymentEntry(unittest.TestCase):
class TestPaymentEntry(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def test_payment_entry_against_order(self):
so = make_sales_order()
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")

View File

@ -1,7 +1,6 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:PLE-{YY}-{MM}-{######}",
"creation": "2022-05-09 19:35:03.334361",
"doctype": "DocType",
"editable_grid": 1,
@ -138,11 +137,10 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-05-19 18:04:44.609115",
"modified": "2022-05-30 19:04:55.532171",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Ledger Entry",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{

View File

@ -6,6 +6,19 @@ import frappe
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
from erpnext.accounts.doctype.gl_entry.gl_entry import (
validate_balance_type,
validate_frozen_account,
)
from erpnext.accounts.utils import update_voucher_outstanding
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
class PaymentLedgerEntry(Document):
def validate_account(self):
@ -18,5 +31,119 @@ class PaymentLedgerEntry(Document):
if not valid_account:
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
def validate_account_details(self):
"""Account must be ledger, active and not freezed"""
ret = frappe.db.sql(
"""select is_group, docstatus, company
from tabAccount where name=%s""",
self.account,
as_dict=1,
)[0]
if ret.is_group == 1:
frappe.throw(
_(
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
).format(self.voucher_type, self.voucher_no, self.account)
)
if ret.docstatus == 2:
frappe.throw(
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
)
if ret.company != self.company:
frappe.throw(
_("{0} {1}: Account {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.account, self.company
)
)
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
for key, value in dimension_filter_map.items():
dimension = key[0]
account = key[1]
if self.account == account:
if value["is_mandatory"] and not self.get(dimension):
frappe.throw(
_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
),
MandatoryAccountDimensionError,
)
if value["allow_or_restrict"] == "Allow":
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
else:
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
if (
account_type == "Profit and Loss"
and self.company == dimension.company
and dimension.mandatory_for_pl
and not dimension.disabled
):
if not self.get(dimension.fieldname):
frappe.throw(
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
dimension.label, self.account
)
)
if (
account_type == "Balance Sheet"
and self.company == dimension.company
and dimension.mandatory_for_bs
and not dimension.disabled
):
if not self.get(dimension.fieldname):
frappe.throw(
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
dimension.label, self.account
)
)
def validate(self):
self.validate_account()
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
self.validate_account_details()
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
# update outstanding amount
if (
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):
update_voucher_outstanding(
self.against_voucher_type, self.against_voucher_no, self.account, self.party_type, self.party
)

View File

@ -39,7 +39,7 @@ def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s
limit %(start)s, %(page_len)s""",
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
)
@ -51,7 +51,7 @@ def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
""" select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='')
limit %(start)s, %(page_len)s""",
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
)

View File

@ -3,16 +3,26 @@
import frappe
from frappe import _, msgprint
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, getdate, nowdate, today
import erpnext
from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
from erpnext.accounts.utils import (
QueryPaymentLedger,
get_outstanding_invoices,
reconcile_against_document,
)
from erpnext.controllers.accounts_controller import get_advance_payment_entries
class PaymentReconciliation(Document):
def __init__(self, *args, **kwargs):
super(PaymentReconciliation, self).__init__(*args, **kwargs)
self.common_filter_conditions = []
@frappe.whitelist()
def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries()
@ -108,54 +118,58 @@ class PaymentReconciliation(Document):
return list(journal_entries)
def get_dr_or_cr_notes(self):
condition = self.get_conditions(get_return_invoices=True)
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
reconciled_dr_or_cr = (
"debit_in_account_currency"
if dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
)
self.build_qb_filter_conditions(get_return_invoices=True)
ple = qb.DocType("Payment Ledger Entry")
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
return frappe.db.sql(
""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
account_currency as currency
FROM `tab{doc}` doc, `tabGL Entry` gl
WHERE
(doc.name = gl.against_voucher or doc.name = gl.voucher_no)
and doc.{party_type_field} = %(party)s
and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
and gl.against_voucher_type = %(voucher_type)s
and doc.docstatus = 1 and gl.party = %(party)s
and gl.party_type = %(party_type)s and gl.account = %(account)s
and gl.is_cancelled = 0 {condition}
GROUP BY doc.name
Having
amount > 0
ORDER BY doc.posting_date
""".format(
doc=voucher_type,
dr_or_cr=dr_or_cr,
reconciled_dr_or_cr=reconciled_dr_or_cr,
party_type_field=frappe.scrub(self.party_type),
condition=condition or "",
),
{
"party": self.party,
"party_type": self.party_type,
"voucher_type": voucher_type,
"account": self.receivable_payable_account,
},
as_dict=1,
if erpnext.get_party_account_type(self.party_type) == "Receivable":
self.common_filter_conditions.append(ple.account_type == "Receivable")
else:
self.common_filter_conditions.append(ple.account_type == "Payable")
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
# get return invoices
doc = qb.DocType(voucher_type)
return_invoices = (
qb.from_(doc)
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
& (IfNull(doc.return_against, "") == "")
)
.run(as_dict=True)
)
outstanding_dr_or_cr = []
if return_invoices:
ple_query = QueryPaymentLedger()
return_outstanding = ple_query.get_voucher_outstandings(
vouchers=return_invoices,
common_filter=self.common_filter_conditions,
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
get_payments=True,
)
for inv in return_outstanding:
if inv.outstanding != 0:
outstanding_dr_or_cr.append(
frappe._dict(
{
"reference_type": inv.voucher_type,
"reference_name": inv.voucher_no,
"amount": -(inv.outstanding),
"posting_date": inv.posting_date,
"currency": inv.currency,
}
)
)
return outstanding_dr_or_cr
def add_payment_entries(self, non_reconciled_payments):
self.set("payments", [])
@ -166,10 +180,15 @@ class PaymentReconciliation(Document):
def get_invoice_entries(self):
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
condition = self.get_conditions(get_invoices=True)
self.build_qb_filter_conditions(get_invoices=True)
non_reconciled_invoices = get_outstanding_invoices(
self.party_type, self.party, self.receivable_payable_account, condition=condition
self.party_type,
self.party,
self.receivable_payable_account,
common_filter=self.common_filter_conditions,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
)
if self.invoice_limit:
@ -329,89 +348,56 @@ class PaymentReconciliation(Document):
if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table"))
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
condition = " and company = '{0}' ".format(self.company)
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear()
ple = qb.DocType("Payment Ledger Entry")
if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
condition = " and cost_center = '{0}' ".format(self.cost_center)
self.common_filter_conditions.append(ple.company == self.company)
if self.get("cost_center") and (get_invoices or get_return_invoices):
self.common_filter_conditions.append(ple.cost_center == self.cost_center)
if get_invoices:
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
if self.from_invoice_date
else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date))
if self.to_invoice_date
else ""
)
dr_or_cr = (
"debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "credit_in_account_currency"
)
if self.minimum_invoice_amount:
condition += " and {dr_or_cr} >= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
)
if self.maximum_invoice_amount:
condition += " and {dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
)
if self.from_invoice_date:
self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date))
if self.to_invoice_date:
self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date))
elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company)
condition += (
" and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
dr_or_cr = (
"debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "credit_in_account_currency"
)
if self.from_payment_date:
self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date))
if self.to_payment_date:
self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date))
if self.minimum_invoice_amount:
condition += " and gl.{dr_or_cr} >= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
)
if self.maximum_invoice_amount:
condition += " and gl.{dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
)
def get_conditions(self, get_payments=False):
condition = " and company = '{0}' ".format(self.company)
else:
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
if self.get("cost_center") and get_payments:
condition = " and cost_center = '{0}' ".format(self.cost_center)
if self.minimum_payment_amount:
condition += (
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
)
if self.maximum_payment_amount:
condition += (
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
)
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
if self.minimum_payment_amount:
condition += (
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
)
if self.maximum_payment_amount:
condition += (
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
)
return condition

View File

@ -4,93 +4,453 @@
import unittest
import frappe
from frappe.utils import add_days, getdate
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.stock.doctype.item.test_item import create_item
class TestPaymentReconciliation(unittest.TestCase):
@classmethod
def setUpClass(cls):
make_customer()
make_invoice_and_payment()
class TestPaymentReconciliation(FrappeTestCase):
def setUp(self):
self.create_company()
self.create_item()
self.create_customer()
self.clear_old_entries()
def test_payment_reconciliation(self):
payment_reco = frappe.get_doc("Payment Reconciliation")
payment_reco.company = "_Test Company"
payment_reco.party_type = "Customer"
payment_reco.party = "_Test Payment Reco Customer"
payment_reco.receivable_payable_account = "Debtors - _TC"
payment_reco.from_invoice_date = add_days(getdate(), -1)
payment_reco.to_invoice_date = getdate()
payment_reco.from_payment_date = add_days(getdate(), -1)
payment_reco.to_payment_date = getdate()
payment_reco.maximum_invoice_amount = 1000
payment_reco.maximum_payment_amount = 1000
payment_reco.invoice_limit = 10
payment_reco.payment_limit = 10
payment_reco.bank_cash_account = "_Test Bank - _TC"
payment_reco.cost_center = "_Test Cost Center - _TC"
payment_reco.get_unreconciled_entries()
def tearDown(self):
frappe.db.rollback()
self.assertEqual(len(payment_reco.get("invoices")), 1)
self.assertEqual(len(payment_reco.get("payments")), 1)
def create_company(self):
company = None
if frappe.db.exists("Company", "_Test Payment Reconciliation"):
company = frappe.get_doc("Company", "_Test Payment Reconciliation")
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test Payment Reconciliation",
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
payment_entry = payment_reco.get("payments")[0].reference_name
invoice = payment_reco.get("invoices")[0].invoice_number
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses - _PR"
self.income_account = "Sales - _PR"
self.expense_account = "Cost of Goods Sold - _PR"
self.debit_to = "Debtors - _PR"
self.creditors = "Creditors - _PR"
payment_reco.allocate_entries(
{
"payments": [payment_reco.get("payments")[0].as_dict()],
"invoices": [payment_reco.get("invoices")[0].as_dict()],
}
# create bank account
if frappe.db.exists("Account", "HDFC - _PR"):
self.bank = "HDFC - _PR"
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - _PR",
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name
def create_item(self):
item = create_item(
item_code="_Test PR Item", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
payment_reco.reconcile()
self.item = item if isinstance(item, str) else item.item_code
payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
def create_customer(self):
if frappe.db.exists("Customer", "_Test PR Customer"):
self.customer = "_Test PR Customer"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer"
customer.type = "Individual"
customer.save()
self.customer = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 2"):
self.customer2 = "_Test PR Customer 2"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 2"
customer.type = "Individual"
customer.save()
self.customer2 = customer.name
def make_customer():
if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "_Test Payment Reco Customer",
"customer_type": "Individual",
"customer_group": "_Test Customer Group",
"territory": "_Test Territory",
}
).insert()
def create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
sinv = create_sales_invoice(
qty=qty,
rate=rate,
company=self.company,
customer=self.customer,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return sinv
def create_payment_entry(self, amount=100, posting_date=nowdate()):
"""
Helper function to populate default values in payment entry
"""
payment = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer,
paid_from=self.debit_to,
paid_to=self.bank,
paid_amount=amount,
)
payment.posting_date = posting_date
return payment
def make_invoice_and_payment():
si = create_sales_invoice(
customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
)
si.cost_center = "_Test Cost Center - _TC"
si.save()
si.submit()
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
"payment_type": "Receive",
"party_type": "Customer",
"party": "_Test Payment Reco Customer",
"company": "_Test Company",
"paid_from_account_currency": "INR",
"paid_to_account_currency": "INR",
"source_exchange_rate": 1,
"target_exchange_rate": 1,
"reference_no": "1",
"reference_date": getdate(),
"received_amount": 690,
"paid_amount": 690,
"paid_from": "Debtors - _TC",
"paid_to": "_Test Bank - _TC",
"cost_center": "_Test Cost Center - _TC",
}
)
pe.insert()
pe.submit()
def create_payment_reconciliation(self):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Customer"
pr.party = self.customer
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
return pr
def create_journal_entry(
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
):
je = frappe.new_doc("Journal Entry")
je.posting_date = posting_date or nowdate()
je.company = self.company
je.user_remark = "test"
if not cost_center:
cost_center = self.cost_center
je.set(
"accounts",
[
{
"account": acc1,
"cost_center": cost_center,
"debit_in_account_currency": amount if amount > 0 else 0,
"credit_in_account_currency": abs(amount) if amount < 0 else 0,
},
{
"account": acc2,
"cost_center": cost_center,
"credit_in_account_currency": amount if amount > 0 else 0,
"debit_in_account_currency": abs(amount) if amount < 0 else 0,
},
],
)
return je
def test_filter_min_max(self):
# check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300)
self.create_sales_invoice(qty=1, rate=400)
self.create_sales_invoice(qty=1, rate=500)
self.create_payment_entry(amount=300).save().submit()
self.create_payment_entry(amount=400).save().submit()
self.create_payment_entry(amount=500).save().submit()
pr = self.create_payment_reconciliation()
pr.minimum_invoice_amount = 400
pr.maximum_invoice_amount = 500
pr.minimum_payment_amount = 300
pr.maximum_payment_amount = 600
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 2)
self.assertEqual(len(pr.get("payments")), 3)
pr.minimum_invoice_amount = 300
pr.maximum_invoice_amount = 600
pr.minimum_payment_amount = 400
pr.maximum_payment_amount = 500
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 3)
self.assertEqual(len(pr.get("payments")), 2)
pr.minimum_invoice_amount = (
pr.maximum_invoice_amount
) = pr.minimum_payment_amount = pr.maximum_payment_amount = 0
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 3)
self.assertEqual(len(pr.get("payments")), 3)
def test_filter_posting_date(self):
# check filter condition using transaction date
date1 = nowdate()
date2 = add_days(nowdate(), -1)
amount = 100
self.create_sales_invoice(qty=1, rate=amount, posting_date=date1)
si2 = self.create_sales_invoice(
qty=1, rate=amount, posting_date=date2, do_not_save=True, do_not_submit=True
)
si2.set_posting_time = 1
si2.posting_date = date2
si2.save().submit()
self.create_payment_entry(amount=amount, posting_date=date1).save().submit()
self.create_payment_entry(amount=amount, posting_date=date2).save().submit()
pr = self.create_payment_reconciliation()
pr.from_invoice_date = pr.to_invoice_date = date1
pr.from_payment_date = pr.to_payment_date = date1
pr.get_unreconciled_entries()
# assert only si and pe are fetched
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 1)
pr.from_invoice_date = date2
pr.to_invoice_date = date1
pr.from_payment_date = date2
pr.to_payment_date = date1
pr.get_unreconciled_entries()
# assert only si and pe are fetched
self.assertEqual(len(pr.get("invoices")), 2)
self.assertEqual(len(pr.get("payments")), 2)
def test_filter_invoice_limit(self):
# check filter condition - invoice limit
transaction_date = nowdate()
rate = 100
invoices = []
payments = []
for i in range(5):
invoices.append(self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date))
pe = self.create_payment_entry(amount=rate, posting_date=transaction_date).save().submit()
payments.append(pe)
pr = self.create_payment_reconciliation()
pr.from_invoice_date = pr.to_invoice_date = transaction_date
pr.from_payment_date = pr.to_payment_date = transaction_date
pr.invoice_limit = 2
pr.payment_limit = 3
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 2)
self.assertEqual(len(pr.get("payments")), 3)
def test_payment_against_invoice(self):
si = self.create_sales_invoice(qty=1, rate=200)
pe = self.create_payment_entry(amount=55).save().submit()
# second payment entry
self.create_payment_entry(amount=35).save().submit()
pr = self.create_payment_reconciliation()
# reconcile multiple payments against invoice
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
si.reload()
self.assertEqual(si.status, "Partly Paid")
# check PR tool output post reconciliation
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 110)
self.assertEqual(pr.get("payments"), [])
# cancel one PE
pe.reload()
pe.cancel()
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 0)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 165)
def test_payment_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
amount = 921
# debit debtors account to record an invoice
je = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
je.accounts[0].party_type = "Customer"
je.accounts[0].party = self.customer
je.save()
je.submit()
self.create_payment_entry(amount=amount, posting_date=transaction_date).save().submit()
pr = self.create_payment_reconciliation()
pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
pr.from_invoice_date = pr.to_invoice_date = transaction_date
pr.from_payment_date = pr.to_payment_date = transaction_date
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_journal_against_invoice(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
# credit debtors account to record a payment
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
je.accounts[1].party_type = "Customer"
je.accounts[1].party = self.customer
je.save()
je.submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# assert outstanding
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_journal_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
amount = 100
# debit debtors account to simulate a invoice
je1 = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
je1.accounts[0].party_type = "Customer"
je1.accounts[0].party = self.customer
je1.save()
je1.submit()
# credit debtors account to simulate a payment
je2 = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer
je2.save()
je2.submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(pr.get("invoices"), [])
self.assertEqual(pr.get("payments"), [])
def test_cr_note_against_invoice(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.is_return = 1
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
pr.get_unreconciled_entries()
# check reconciliation tool output
# reconciled invoice and credit note shouldn't show up in selection
self.assertEqual(pr.get("invoices"), [])
self.assertEqual(pr.get("payments"), [])
# assert outstanding
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
def test_cr_note_partial_against_invoice(self):
transaction_date = nowdate()
amount = 100
allocated_amount = 80
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.is_return = 1
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = allocated_amount
pr.reconcile()
# assert outstanding
si.reload()
self.assertEqual(si.status, "Partly Paid")
self.assertEqual(si.outstanding_amount, 20)
pr.get_unreconciled_entries()
# check reconciliation tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 1)
self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20)
self.assertEqual(pr.get("payments")[0].amount, 20)

View File

@ -54,8 +54,8 @@ class PeriodClosingVoucher(AccountsController):
pce = frappe.db.sql(
"""select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1""",
(self.posting_date, self.fiscal_year),
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
(self.posting_date, self.fiscal_year, self.company),
)
if pce and pce[0][0]:
frappe.throw(

View File

@ -173,7 +173,7 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
where
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
and (pf.name like %(txt)s)
and pf.disabled = 0 limit %(start)s, %(page_len)s""",
and pf.disabled = 0 limit %(page_len)s offset %(start)s""",
args,
)

View File

@ -36,8 +36,12 @@ class PricingRule(Document):
def validate_duplicate_apply_on(self):
if self.apply_on != "Transaction":
field = apply_on_dict.get(self.apply_on)
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
apply_on_table = apply_on_dict.get(self.apply_on)
if not apply_on_table:
return
apply_on_field = frappe.scrub(self.apply_on)
values = [d.get(apply_on_field) for d in self.get(apply_on_table) if d.get(apply_on_field)]
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))

View File

@ -165,17 +165,6 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).set_missing_values(for_validate)
def check_conversion_rate(self):
default_currency = erpnext.get_company_currency(self.company)
if not default_currency:
throw(_("Please enter default currency in Company Master"))
if (
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
or not self.conversion_rate
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
):
throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self):
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)

View File

@ -1616,6 +1616,26 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice")
pi.supplier = "_Test Supplier"
pi.company = "_Test Company"
pi.append(
"items",
{
"item_name": "Opening item",
"qty": 1,
"uom": "Tonne",
"stock_uom": "Kg",
"rate": 1000,
"expense_account": "Stock Received But Not Billed - _TC",
},
)
pi.save()
self.assertEqual(pi.items[0].conversion_factor, 1000)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@ -195,6 +195,7 @@
"label": "Rejected Qty"
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
@ -214,6 +215,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "UOM Conversion Factor",
@ -222,6 +224,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Accepted Qty in Stock UOM",
@ -871,7 +874,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-11-15 17:04:07.191013",
"modified": "2022-06-17 05:31:10.520171",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@ -879,5 +882,6 @@
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@ -1790,6 +1790,8 @@
"width": "50%"
},
{
"fetch_from": "sales_partner.commission_rate",
"fetch_if_empty": 1,
"fieldname": "commission_rate",
"fieldtype": "Float",
"hide_days": 1,
@ -2038,7 +2040,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2022-03-08 16:08:53.517903",
"modified": "2022-06-10 03:52:51.409913",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -114,6 +114,7 @@ class SalesInvoice(SellingController):
self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
self.validate_income_account()
self.check_conversion_rate()
validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_invoice_reference

View File

@ -1583,6 +1583,17 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gle)
def test_invoice_exchange_rate(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=1,
do_not_save=1,
)
self.assertRaises(frappe.ValidationError, si.save)
def test_invalid_currency(self):
# Customer currency = USD

View File

@ -182,6 +182,7 @@
"oldfieldtype": "Currency"
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
@ -200,6 +201,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "UOM Conversion Factor",
@ -207,6 +209,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Qty as per Stock UOM",
@ -843,7 +846,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-03-23 08:18:04.928287",
"modified": "2022-06-17 05:33:15.335912",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@ -145,13 +145,14 @@ class Subscription(Document):
You shouldn't need to call this directly. Use `get_billing_cycle` instead.
"""
plan_names = [plan.plan for plan in self.plans]
billing_info = frappe.db.sql(
"select distinct `billing_interval`, `billing_interval_count` "
"from `tabSubscription Plan` "
"where name in %s",
(plan_names,),
as_dict=1,
)
subscription_plan = frappe.qb.DocType("Subscription Plan")
billing_info = (
frappe.qb.from_(subscription_plan)
.select(subscription_plan.billing_interval, subscription_plan.billing_interval_count)
.distinct()
.where(subscription_plan.name.isin(plan_names))
).run(as_dict=1)
return billing_info

View File

@ -35,7 +35,13 @@ def make_gl_entries(
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
create_payment_ledger_entry(gl_map)
create_payment_ledger_entry(
gl_map,
cancel=0,
adv_adj=adv_adj,
update_outstanding=update_outstanding,
from_repost=from_repost,
)
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map proccess there may no be any GL Entries
elif gl_map:
@ -482,6 +488,9 @@ def make_reverse_gl_entries(
if gl_entries:
create_payment_ledger_entry(gl_entries, cancel=1)
create_payment_ledger_entry(
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
)
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])

View File

@ -13,7 +13,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
"idx": 0,
"is_complete": 0,
"modified": "2022-06-07 14:29:21.352132",
"modified": "2022-06-14 17:38:24.967834",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts",

View File

@ -2,14 +2,14 @@
"action": "Create Entry",
"action_label": "Manage Sales Tax Templates",
"creation": "2020-05-13 19:29:43.844463",
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n",
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2022-06-07 14:27:15.906286",
"modified": "2022-06-14 17:37:56.694261",
"modified_by": "Administrator",
"name": "Setup Taxes",
"owner": "Administrator",

View File

@ -211,7 +211,7 @@ def set_address_details(
else:
party_details.update(get_company_address(company))
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order"]:
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]:
if party_details.company_address:
party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address)

View File

@ -42,7 +42,7 @@
{% if(filters.show_future_payments) { %}
{% var balance_row = data.slice(-1).pop();
var start = filters.based_on_payment_terms ? 13 : 11;
var start = report.columns.findIndex((elem) => (elem.fieldname == 'age'));
var range1 = report.columns[start].label;
var range2 = report.columns[start+1].label;
var range3 = report.columns[start+2].label;

View File

@ -172,11 +172,6 @@ frappe.query_reports["Accounts Receivable"] = {
"label": __("Show Sales Person"),
"fieldtype": "Check",
},
{
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),

View File

@ -5,7 +5,9 @@
from collections import OrderedDict
import frappe
from frappe import _, scrub
from frappe import _, qb, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date
from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@ -41,6 +43,8 @@ def execute(filters=None):
class ReceivablePayableReport(object):
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
self.qb_selection_filter = []
self.ple = qb.DocType("Payment Ledger Entry")
self.filters.report_date = getdate(self.filters.report_date or nowdate())
self.age_as_on = (
getdate(nowdate())
@ -78,7 +82,7 @@ class ReceivablePayableReport(object):
self.skip_total_row = 1
def get_data(self):
self.get_gl_entries()
self.get_ple_entries()
self.get_sales_invoices_or_customers_based_on_sales_person()
self.voucher_balance = OrderedDict()
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
@ -96,25 +100,25 @@ class ReceivablePayableReport(object):
self.get_return_entries()
self.data = []
for gle in self.gl_entries:
self.update_voucher_balance(gle)
for ple in self.ple_entries:
self.update_voucher_balance(ple)
self.build_data()
def init_voucher_balance(self):
# build all keys, since we want to exclude vouchers beyond the report date
for gle in self.gl_entries:
for ple in self.ple_entries:
# get the balance object for voucher_type
key = (gle.voucher_type, gle.voucher_no, gle.party)
key = (ple.voucher_type, ple.voucher_no, ple.party)
if not key in self.voucher_balance:
self.voucher_balance[key] = frappe._dict(
voucher_type=gle.voucher_type,
voucher_no=gle.voucher_no,
party=gle.party,
party_account=gle.account,
posting_date=gle.posting_date,
account_currency=gle.account_currency,
remarks=gle.remarks if self.filters.get("show_remarks") else None,
voucher_type=ple.voucher_type,
voucher_no=ple.voucher_no,
party=ple.party,
party_account=ple.account,
posting_date=ple.posting_date,
account_currency=ple.account_currency,
invoiced=0.0,
paid=0.0,
credit_note=0.0,
@ -124,23 +128,22 @@ class ReceivablePayableReport(object):
credit_note_in_account_currency=0.0,
outstanding_in_account_currency=0.0,
)
self.get_invoices(gle)
if self.filters.get("group_by_party"):
self.init_subtotal_row(gle.party)
self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party"):
self.init_subtotal_row("Total")
def get_invoices(self, gle):
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"):
def get_invoices(self, ple):
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
if self.filters.get("sales_person"):
if gle.voucher_no in self.sales_person_records.get(
if ple.voucher_no in self.sales_person_records.get(
"Sales Invoice", []
) or gle.party in self.sales_person_records.get("Customer", []):
self.invoices.add(gle.voucher_no)
) or ple.party in self.sales_person_records.get("Customer", []):
self.invoices.add(ple.voucher_no)
else:
self.invoices.add(gle.voucher_no)
self.invoices.add(ple.voucher_no)
def init_subtotal_row(self, party):
if not self.total_row_map.get(party):
@ -162,39 +165,49 @@ class ReceivablePayableReport(object):
"range5",
]
def update_voucher_balance(self, gle):
def get_voucher_balance(self, ple):
if self.filters.get("sales_person"):
if not (
ple.party in self.sales_person_records.get("Customer", [])
or ple.against_voucher_no in self.sales_person_records.get("Sales Invoice", [])
):
return
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
row = self.voucher_balance.get(key)
return row
def update_voucher_balance(self, ple):
# get the row where this balance needs to be updated
# if its a payment, it will return the linked invoice or will be considered as advance
row = self.get_voucher_balance(gle)
row = self.get_voucher_balance(ple)
if not row:
return
# gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle)
gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
if gle_balance > 0:
if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher:
# debit against sales / purchase invoice
row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
amount = ple.amount
amount_in_account_currency = ple.amount_in_account_currency
# update voucher
if ple.amount > 0:
if (
ple.voucher_type in ["Journal Entry", "Payment Entry"]
and ple.voucher_no != ple.against_voucher_no
):
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
else:
# invoice
row.invoiced += gle_balance
row.invoiced_in_account_currency += gle_balance_in_account_currency
row.invoiced += amount
row.invoiced_in_account_currency += amount_in_account_currency
else:
# payment or credit note for receivables
if self.is_invoice(gle):
# stand alone debit / credit note
row.credit_note -= gle_balance
row.credit_note_in_account_currency -= gle_balance_in_account_currency
if self.is_invoice(ple):
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
else:
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
if gle.cost_center:
row.cost_center = str(gle.cost_center)
if ple.cost_center:
row.cost_center = str(ple.cost_center)
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
@ -210,39 +223,6 @@ class ReceivablePayableReport(object):
self.data.append({})
self.update_sub_total_row(sub_total_row, "Total")
def get_voucher_balance(self, gle):
if self.filters.get("sales_person"):
against_voucher = gle.against_voucher or gle.voucher_no
if not (
gle.party in self.sales_person_records.get("Customer", [])
or against_voucher in self.sales_person_records.get("Sales Invoice", [])
):
return
voucher_balance = None
if gle.against_voucher:
# find invoice
against_voucher = gle.against_voucher
# If payment is made against credit note
# and credit note is made against a Sales Invoice
# then consider the payment against original sales invoice.
if gle.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
if gle.against_voucher in self.return_entries:
return_against = self.return_entries.get(gle.against_voucher)
if return_against:
against_voucher = return_against
voucher_balance = self.voucher_balance.get(
(gle.against_voucher_type, against_voucher, gle.party)
)
if not voucher_balance:
# no invoice, this is an invoice / stand-alone payment / credit note
voucher_balance = self.voucher_balance.get((gle.voucher_type, gle.voucher_no, gle.party))
return voucher_balance
def build_data(self):
# set outstanding for all the accumulated balances
# as we can use this to filter out invoices without outstanding
@ -260,6 +240,7 @@ class ReceivablePayableReport(object):
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
):
# non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms:
@ -669,48 +650,53 @@ class ReceivablePayableReport(object):
index = 4
row["range" + str(index + 1)] = row.outstanding
def get_gl_entries(self):
def get_ple_entries(self):
# get all the GL entries filtered by the given filters
conditions, values = self.prepare_conditions()
order_by = self.get_order_by_condition()
self.prepare_conditions()
if self.filters.show_future_payments:
values.insert(2, self.filters.report_date)
date_condition = """AND (posting_date <= %s
OR (against_voucher IS NULL AND DATE(creation) <= %s))"""
self.qb_selection_filter.append(
(
self.ple.posting_date.lte(self.filters.report_date)
| (
(self.ple.voucher_no == self.ple.against_voucher_no)
& (Date(self.ple.creation).lte(self.filters.report_date))
)
)
)
else:
date_condition = "AND posting_date <=%s"
self.qb_selection_filter.append(self.ple.posting_date.lte(self.filters.report_date))
if self.filters.get(scrub(self.party_type)):
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit"
else:
select_fields = "debit, credit"
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql(
"""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
from
`tabGL Entry`
where
docstatus < 2
and is_cancelled = 0
and party_type=%s
and (party is not null and party != '')
{2} {3} {4}""".format(
select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks
),
values,
as_dict=True,
ple = qb.DocType("Payment Ledger Entry")
query = (
qb.from_(ple)
.select(
ple.account,
ple.voucher_type,
ple.voucher_no,
ple.against_voucher_type,
ple.against_voucher_no,
ple.party_type,
ple.cost_center,
ple.party,
ple.posting_date,
ple.due_date,
ple.account_currency.as_("currency"),
ple.amount,
ple.amount_in_account_currency,
)
.where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter))
)
if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date)
else:
query = query.orderby(self.ple.posting_date, self.ple.party)
self.ple_entries = query.run(as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
@ -731,23 +717,21 @@ class ReceivablePayableReport(object):
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
def prepare_conditions(self):
conditions = [""]
values = [self.party_type, self.filters.report_date]
self.qb_selection_filter = []
party_type_field = scrub(self.party_type)
self.add_common_filters(conditions, values, party_type_field)
self.add_common_filters(party_type_field=party_type_field)
if party_type_field == "customer":
self.add_customer_filters(conditions, values)
self.add_customer_filters()
elif party_type_field == "supplier":
self.add_supplier_filters(conditions, values)
self.add_supplier_filters()
if self.filters.cost_center:
self.get_cost_center_conditions(conditions)
self.get_cost_center_conditions()
self.add_accounting_dimensions_filters(conditions, values)
return " and ".join(conditions), values
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self, conditions):
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
@ -755,32 +739,20 @@ class ReceivablePayableReport(object):
center.name
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
]
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
cost_center_string = '", "'.join(cost_center_list)
conditions.append('cost_center in ("{0}")'.format(cost_center_string))
def get_order_by_condition(self):
if self.filters.get("group_by_party"):
return "order by party, posting_date"
else:
return "order by posting_date, party"
def add_common_filters(self, conditions, values, party_type_field):
def add_common_filters(self, party_type_field):
if self.filters.company:
conditions.append("company=%s")
values.append(self.filters.company)
self.qb_selection_filter.append(self.ple.company == self.filters.company)
if self.filters.finance_book:
conditions.append("ifnull(finance_book, '') in (%s, '')")
values.append(self.filters.finance_book)
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
if self.filters.get(party_type_field):
conditions.append("party=%s")
values.append(self.filters.get(party_type_field))
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
if self.filters.party_account:
conditions.append("account =%s")
values.append(self.filters.party_account)
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
else:
# get GL with "receivable" or "payable" account_type
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
@ -792,46 +764,68 @@ class ReceivablePayableReport(object):
]
if accounts:
conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts)))
values += accounts
self.qb_selection_filter.append(self.ple.account.isin(accounts))
def add_customer_filters(
self,
):
self.customter = qb.DocType("Customer")
def add_customer_filters(self, conditions, values):
if self.filters.get("customer_group"):
conditions.append(self.get_hierarchical_filters("Customer Group", "customer_group"))
self.get_hierarchical_filters("Customer Group", "customer_group")
if self.filters.get("territory"):
conditions.append(self.get_hierarchical_filters("Territory", "territory"))
self.get_hierarchical_filters("Territory", "territory")
if self.filters.get("payment_terms_template"):
conditions.append("party in (select name from tabCustomer where payment_terms=%s)")
values.append(self.filters.get("payment_terms_template"))
self.qb_selection_filter.append(
self.ple.party_isin(
qb.from_(self.customer).where(
self.customer.payment_terms == self.filters.get("payment_terms_template")
)
)
)
if self.filters.get("sales_partner"):
conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)")
values.append(self.filters.get("sales_partner"))
def add_supplier_filters(self, conditions, values):
if self.filters.get("supplier_group"):
conditions.append(
"""party in (select name from tabSupplier
where supplier_group=%s)"""
self.qb_selection_filter.append(
self.ple.party_isin(
qb.from_(self.customer).where(
self.customer.default_sales_partner == self.filters.get("payment_terms_template")
)
)
)
def add_supplier_filters(self):
supplier = qb.DocType("Supplier")
if self.filters.get("supplier_group"):
self.qb_selection_filter.append(
self.ple.party.isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.supplier_group == self.filters.get("supplier_group"))
)
)
values.append(self.filters.get("supplier_group"))
if self.filters.get("payment_terms_template"):
conditions.append("party in (select name from tabSupplier where payment_terms=%s)")
values.append(self.filters.get("payment_terms_template"))
self.qb_selection_filter.append(
self.ple.party.isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.payment_terms == self.filters.get("supplier_group"))
)
)
def get_hierarchical_filters(self, doctype, key):
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
return """party in (select name from tabCustomer
where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt}
and name=tabCustomer.{key}))""".format(
doctype=doctype, lft=lft, rgt=rgt, key=key
)
doc = qb.DocType(doctype)
ple = self.ple
customer = self.customer
groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt))
customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups))
self.qb_selection_filter.append(ple.isin(ple.party.isin(customers)))
def add_accounting_dimensions_filters(self, conditions, values):
def add_accounting_dimensions_filters(self):
accounting_dimensions = get_accounting_dimensions(as_list=False)
if accounting_dimensions:
@ -841,30 +835,16 @@ class ReceivablePayableReport(object):
self.filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, self.filters.get(dimension.fieldname)
)
conditions.append("{0} in %s".format(dimension.fieldname))
values.append(tuple(self.filters.get(dimension.fieldname)))
self.qb_selection_filter.append(
self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
)
else:
self.qb_selection_filter.append(
self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
)
def get_gle_balance(self, gle):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
def get_gle_balance_in_account_currency(self, gle):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(
self.dr_or_cr + "_in_account_currency"
) - self.get_reverse_balance_in_account_currency(gle)
def get_reverse_balance_in_account_currency(self, gle):
return gle.get(
"debit_in_account_currency" if self.dr_or_cr == "credit" else "credit_in_account_currency"
)
def get_reverse_balance(self, gle):
# get "credit" balance if report type is "debit" and vice versa
return gle.get("debit" if self.dr_or_cr == "credit" else "credit")
def is_invoice(self, gle):
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"):
def is_invoice(self, ple):
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
return True
def get_party_details(self, party):
@ -926,9 +906,6 @@ class ReceivablePayableReport(object):
width=180,
)
if self.filters.show_remarks:
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
self.add_column(label="Due Date", fieldtype="Date")
if self.party_type == "Supplier":

View File

@ -12,6 +12,7 @@ class TestAccountsReceivable(unittest.TestCase):
def test_accounts_receivable(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
filters = {
"company": "_Test Company 2",

View File

@ -50,7 +50,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
"reqd": 1,
on_change: () => {
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) {
let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date");
frappe.query_report.set_filter_value({
period_start_date: year_start_date
});
});
}
},
{
"fieldname":"to_fiscal_year",
@ -58,7 +66,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
"reqd": 1,
on_change: () => {
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) {
let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date");
frappe.query_report.set_filter_value({
period_end_date: year_end_date
});
});
}
},
{
"fieldname":"finance_book",

View File

@ -35,7 +35,7 @@ frappe.query_reports["Gross Profit"] = {
"fieldname":"group_by",
"label": __("Group By"),
"fieldtype": "Select",
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject",
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term",
"default": "Invoice"
},
],

View File

@ -4,7 +4,7 @@
import frappe
from frappe import _, scrub
from frappe.utils import cint, flt
from frappe.utils import cint, flt, formatdate
from erpnext.controllers.queries import get_match_cond
from erpnext.stock.utils import get_incoming_rate
@ -124,6 +124,23 @@ def execute(filters=None):
"gross_profit",
"gross_profit_percent",
],
"monthly": [
"monthly",
"qty",
"base_rate",
"buying_rate",
"base_amount",
"buying_amount",
"gross_profit",
"gross_profit_percent",
],
"payment_term": [
"payment_term",
"base_amount",
"buying_amount",
"gross_profit",
"gross_profit_percent",
],
}
)
@ -317,6 +334,19 @@ def get_columns(group_wise_columns, filters):
"options": "territory",
"width": 100,
},
"monthly": {
"label": _("Monthly"),
"fieldname": "monthly",
"fieldtype": "Data",
"width": 100,
},
"payment_term": {
"label": _("Payment Term"),
"fieldname": "payment_term",
"fieldtype": "Link",
"options": "Payment Term",
"width": 170,
},
}
)
@ -390,6 +420,9 @@ class GrossProfitGenerator(object):
buying_amount = 0
for row in reversed(self.si_list):
if self.filters.get("group_by") == "Monthly":
row.monthly = formatdate(row.posting_date, "MMM YYYY")
if self.skip_row(row):
continue
@ -445,17 +478,7 @@ class GrossProfitGenerator(object):
def get_average_rate_based_on_group_by(self):
for key in list(self.grouped):
if self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]):
if i == 0:
new_row = row
else:
new_row.qty += flt(row.qty)
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
else:
if self.filters.get("group_by") == "Invoice":
for i, row in enumerate(self.grouped[key]):
if row.indent == 1.0:
if (
@ -469,6 +492,44 @@ class GrossProfitGenerator(object):
if flt(row.qty) or row.base_amount:
row = self.set_average_rate(row)
self.grouped_data.append(row)
elif self.filters.get("group_by") == "Payment Term":
for i, row in enumerate(self.grouped[key]):
invoice_portion = 0
if row.is_return:
invoice_portion = 100
elif row.invoice_portion:
invoice_portion = row.invoice_portion
else:
invoice_portion = row.payment_amount * 100 / row.base_net_amount
if i == 0:
new_row = row
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
else:
new_row.qty += flt(row.qty)
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
else:
for i, row in enumerate(self.grouped[key]):
if i == 0:
new_row = row
else:
new_row.qty += flt(row.qty)
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
def set_average_based_on_payment_term_portion(self, new_row, row, invoice_portion, aggr=False):
cols = ["base_amount", "buying_amount", "gross_profit"]
for col in cols:
if aggr:
new_row[col] += row[col] * invoice_portion / 100
else:
new_row[col] = row[col] * invoice_portion / 100
def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get(
@ -622,6 +683,20 @@ class GrossProfitGenerator(object):
sales_person_cols = ""
sales_team_table = ""
if self.filters.group_by == "Payment Term":
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
'{0}',
coalesce(schedule.payment_term, '{1}')) as payment_term,
schedule.invoice_portion,
schedule.payment_amount """.format(
_("Sales Return"), _("No Terms")
)
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
`tabSales Invoice`.is_return = 0 """
else:
payment_term_cols = ""
payment_term_table = ""
if self.filters.get("sales_invoice"):
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
@ -644,10 +719,12 @@ class GrossProfitGenerator(object):
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
`tabSales Invoice Item`.cost_center
{sales_person_cols}
{payment_term_cols}
from
`tabSales Invoice` inner join `tabSales Invoice Item`
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
{sales_team_table}
{payment_term_table}
where
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
order by
@ -655,6 +732,8 @@ class GrossProfitGenerator(object):
conditions=conditions,
sales_person_cols=sales_person_cols,
sales_team_table=sales_team_table,
payment_term_cols=payment_term_cols,
payment_term_table=payment_term_table,
match_cond=get_match_cond("Sales Invoice"),
),
self.filters,

View File

@ -100,7 +100,7 @@ def get_sales_details(filters):
sales_data = frappe.db.sql(
"""
select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date,
DATEDIFF(CURDATE(), {date_field}) as days_since_last_order
DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order
from `tab{doctype}` s, `tab{doctype} Item` si
where s.name = si.parent and s.docstatus = 1
order by days_since_last_order """.format( # nosec

View File

@ -179,7 +179,7 @@ def get_sales_invoice_data(filters):
def get_mode_of_payments(filters):
mode_of_payments = {}
invoice_list = get_invoices(filters)
invoice_list_names = ",".join('"' + invoice["name"] + '"' for invoice in invoice_list)
invoice_list_names = ",".join("'" + invoice["name"] + "'" for invoice in invoice_list)
if invoice_list:
inv_mop = frappe.db.sql(
"""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
@ -200,7 +200,7 @@ def get_mode_of_payments(filters):
from `tabJournal Entry` a, `tabJournal Entry Account` b
where a.name = b.parent
and a.docstatus = 1
and b.reference_type = "Sales Invoice"
and b.reference_type = 'Sales Invoice'
and b.reference_name in ({invoice_list_names})
""".format(
invoice_list_names=invoice_list_names
@ -228,7 +228,7 @@ def get_invoices(filters):
def get_mode_of_payment_details(filters):
mode_of_payment_details = {}
invoice_list = get_invoices(filters)
invoice_list_names = ",".join('"' + invoice["name"] + '"' for invoice in invoice_list)
invoice_list_names = ",".join("'" + invoice["name"] + "'" for invoice in invoice_list)
if invoice_list:
inv_mop_detail = frappe.db.sql(
"""
@ -259,7 +259,7 @@ def get_mode_of_payment_details(filters):
from `tabJournal Entry` a, `tabJournal Entry Account` b
where a.name = b.parent
and a.docstatus = 1
and b.reference_type = "Sales Invoice"
and b.reference_type = 'Sales Invoice'
and b.reference_name in ({invoice_list_names})
group by a.owner, a.posting_date, mode_of_payment
) t

View File

@ -160,14 +160,12 @@ def get_rootwise_opening_balances(filters, report_type):
if filters.project:
additional_conditions += " and project = %(project)s"
if filters.finance_book:
fb_conditions = " AND finance_book = %(finance_book)s"
if filters.include_default_book_entries:
fb_conditions = (
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
additional_conditions += fb_conditions
if filters.get("include_default_book_entries"):
additional_conditions += (
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
else:
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
accounting_dimensions = get_accounting_dimensions(as_list=False)

View File

@ -62,8 +62,8 @@ class TestUtils(unittest.TestCase):
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
for doc in (se1, se2, se3):
vouchers.append((doc.doctype, doc.name))

View File

@ -3,13 +3,28 @@
from json import loads
from typing import List, Tuple
from typing import TYPE_CHECKING, List, Optional, Tuple
import frappe
import frappe.defaults
from frappe import _, qb, throw
from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
from frappe.query_builder import AliasedQuery, Criterion, Table
from frappe.query_builder.functions import Sum
from frappe.query_builder.utils import DocType
from frappe.utils import (
cint,
create_batch,
cstr,
flt,
formatdate,
get_number_format_info,
getdate,
now,
nowdate,
)
from pypika import Order
from pypika.terms import ExistsCriterion
import erpnext
@ -19,6 +34,9 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on
if TYPE_CHECKING:
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
class FiscalYearError(frappe.ValidationError):
pass
@ -28,6 +46,9 @@ class PaymentEntryUnlinkError(frappe.ValidationError):
pass
GL_REPOSTING_CHUNK = 100
@frappe.whitelist()
def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
@ -42,37 +63,32 @@ def get_fiscal_years(
if not fiscal_years:
# if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate)
cond = ""
if fiscal_year:
cond += " and fy.name = {0}".format(frappe.db.escape(fiscal_year))
if company:
cond += """
and (not exists (select name
from `tabFiscal Year Company` fyc
where fyc.parent = fy.name)
or exists(select company
from `tabFiscal Year Company` fyc
where fyc.parent = fy.name
and fyc.company=%(company)s)
)
"""
FY = DocType("Fiscal Year")
fiscal_years = frappe.db.sql(
"""
select
fy.name, fy.year_start_date, fy.year_end_date
from
`tabFiscal Year` fy
where
disabled = 0 {0}
order by
fy.year_start_date desc""".format(
cond
),
{"company": company},
as_dict=True,
query = (
frappe.qb.from_(FY)
.select(FY.name, FY.year_start_date, FY.year_end_date)
.where(FY.disabled == 0)
)
if fiscal_year:
query = query.where(FY.name == fiscal_year)
if company:
FYC = DocType("Fiscal Year Company")
query = query.where(
ExistsCriterion(frappe.qb.from_(FYC).select(FYC.name).where(FYC.parent == FY.name)).negate()
| ExistsCriterion(
frappe.qb.from_(FYC)
.select(FYC.company)
.where(FYC.parent == FY.name)
.where(FYC.company == company)
)
)
query = query.orderby(FY.year_start_date, Order.desc)
fiscal_years = query.run(as_dict=True)
frappe.cache().hset("fiscal_years", company, fiscal_years)
if not transaction_date and not fiscal_year:
@ -423,7 +439,8 @@ def reconcile_against_document(args):
# cancel advance entry
doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True
doc.make_gl_entries(cancel=1, adv_adj=1)
gl_map = doc.build_gl_map()
create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1)
for entry in entries:
check_if_advance_entry_modified(entry)
@ -438,7 +455,9 @@ def reconcile_against_document(args):
doc.save(ignore_permissions=True)
# re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
doc.make_gl_entries(cancel=0, adv_adj=1)
gl_map = doc.build_gl_map()
create_payment_ledger_entry(gl_map, cancel=0, adv_adj=1)
frappe.flags.ignore_party_validation = False
if entry.voucher_type in ("Payment Entry", "Journal Entry"):
@ -461,7 +480,7 @@ def check_if_advance_entry_modified(args):
select t2.{dr_or_cr} from `tabJournal Entry` t1, `tabJournal Entry Account` t2
where t1.name = t2.parent and t2.account = %(account)s
and t2.party_type = %(party_type)s and t2.party = %(party)s
and (t2.reference_type is null or t2.reference_type in ("", "Sales Order", "Purchase Order"))
and (t2.reference_type is null or t2.reference_type in ('', 'Sales Order', 'Purchase Order'))
and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
and t1.docstatus=1 """.format(
dr_or_cr=args.get("dr_or_cr")
@ -481,7 +500,7 @@ def check_if_advance_entry_modified(args):
t1.name = t2.parent and t1.docstatus = 1
and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s
and t2.reference_doctype in ("", "Sales Order", "Purchase Order")
and t2.reference_doctype in ('', 'Sales Order', 'Purchase Order')
and t2.allocated_amount = %(unreconciled_amount)s
""".format(
party_account_field
@ -802,7 +821,11 @@ def get_held_invoices(party_type, party):
return held_invoices
def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
def get_outstanding_invoices(
party_type, party, account, common_filter=None, min_outstanding=None, max_outstanding=None
):
ple = qb.DocType("Payment Ledger Entry")
outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
@ -815,76 +838,30 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
else:
party_account_type = erpnext.get_party_account_type(party_type)
if party_account_type == "Receivable":
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
else:
dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
held_invoices = get_held_invoices(party_type, party)
invoice_list = frappe.db.sql(
"""
select
voucher_no, voucher_type, posting_date, due_date,
ifnull(sum({dr_or_cr}), 0) as invoice_amount,
account_currency as currency
from
`tabGL Entry`
where
party_type = %(party_type)s and party = %(party)s
and account = %(account)s and {dr_or_cr} > 0
and is_cancelled=0
{condition}
and ((voucher_type = 'Journal Entry'
and (against_voucher = '' or against_voucher is null))
or (voucher_type not in ('Journal Entry', 'Payment Entry')))
group by voucher_type, voucher_no
order by posting_date, name""".format(
dr_or_cr=dr_or_cr, condition=condition or ""
),
{
"party_type": party_type,
"party": party,
"account": account,
},
as_dict=True,
)
common_filter = common_filter or []
common_filter.append(ple.account_type == party_account_type)
common_filter.append(ple.account == account)
common_filter.append(ple.party_type == party_type)
common_filter.append(ple.party == party)
payment_entries = frappe.db.sql(
"""
select against_voucher_type, against_voucher,
ifnull(sum({payment_dr_or_cr}), 0) as payment_amount
from `tabGL Entry`
where party_type = %(party_type)s and party = %(party)s
and account = %(account)s
and {payment_dr_or_cr} > 0
and against_voucher is not null and against_voucher != ''
and is_cancelled=0
group by against_voucher_type, against_voucher
""".format(
payment_dr_or_cr=payment_dr_or_cr
),
{"party_type": party_type, "party": party, "account": account},
as_dict=True,
ple_query = QueryPaymentLedger()
invoice_list = ple_query.get_voucher_outstandings(
common_filter=common_filter,
min_outstanding=min_outstanding,
max_outstanding=max_outstanding,
get_invoices=True,
)
pe_map = frappe._dict()
for d in payment_entries:
pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount)
for d in invoice_list:
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
payment_amount = d.invoice_amount - d.outstanding
outstanding_amount = d.outstanding
if outstanding_amount > 0.5 / (10**precision):
if (
filters
and filters.get("outstanding_amt_greater_than")
and not (
outstanding_amount >= filters.get("outstanding_amt_greater_than")
and outstanding_amount <= filters.get("outstanding_amt_less_than")
)
min_outstanding
and max_outstanding
and not (outstanding_amount >= min_outstanding and outstanding_amount <= max_outstanding)
):
continue
@ -1122,38 +1099,62 @@ def update_gl_entries_after(
def repost_gle_for_stock_vouchers(
stock_vouchers, posting_date, company=None, warehouse_account=None
stock_vouchers: List[Tuple[str, str]],
posting_date: str,
company: Optional[str] = None,
warehouse_account=None,
repost_doc: Optional["RepostItemValuation"] = None,
):
from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative
if not stock_vouchers:
return
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if repost_doc and repost_doc.gl_reposting_index:
# Restore progress
stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision
):
for stock_vouchers_chunk in create_batch(stock_vouchers, GL_REPOSTING_CHUNK):
gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date)
for voucher_type, voucher_no in stock_vouchers_chunk:
existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
# Some transactions post credit as negative debit, this is handled while posting GLE
# but while comparing we need to make sure it's flipped so comparisons are accurate
expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision
):
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no)
if not frappe.flags.in_test:
frappe.db.commit()
if repost_doc:
repost_doc.db_set(
"gl_reposting_index",
cint(repost_doc.gl_reposting_index) + len(stock_vouchers_chunk),
)
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
def sort_stock_vouchers_by_posting_date(
@ -1167,6 +1168,9 @@ def sort_stock_vouchers_by_posting_date(
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_date)
.orderby(sle.posting_time)
.orderby(sle.creation)
).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
@ -1348,7 +1352,9 @@ def check_and_delete_linked_reports(report):
frappe.delete_doc("Desktop Icon", icon)
def create_payment_ledger_entry(gl_entries, cancel=0):
def create_payment_ledger_entry(
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0
):
if gl_entries:
ple = None
@ -1421,9 +1427,42 @@ def create_payment_ledger_entry(gl_entries, cancel=0):
if cancel:
delink_original_entry(ple)
ple.flags.ignore_permissions = 1
ple.flags.adv_adj = adv_adj
ple.flags.from_repost = from_repost
ple.flags.update_outstanding = update_outstanding
ple.submit()
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
ple = frappe.qb.DocType("Payment Ledger Entry")
vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})]
common_filter = []
if account:
common_filter.append(ple.account == account)
if party_type:
common_filter.append(ple.party_type == party_type)
if party:
common_filter.append(ple.party == party)
ple_query = QueryPaymentLedger()
# on cancellation outstanding can be an empty list
voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter)
if voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] and voucher_outstanding:
outstanding = voucher_outstanding[0]
ref_doc = frappe.get_doc(voucher_type, voucher_no)
# Didn't use db_set for optimisation purpose
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"]
frappe.db.set_value(
voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"]
)
ref_doc.set_status(update=True)
def delink_original_entry(pl_entry):
if pl_entry:
ple = qb.DocType("Payment Ledger Entry")
@ -1445,3 +1484,196 @@ def delink_original_entry(pl_entry):
)
)
query.run()
class QueryPaymentLedger(object):
"""
Helper Class for Querying Payment Ledger Entry
"""
def __init__(self):
self.ple = qb.DocType("Payment Ledger Entry")
# query result
self.voucher_outstandings = []
# query filters
self.vouchers = []
self.common_filter = []
self.min_outstanding = None
self.max_outstanding = None
def reset(self):
# clear filters
self.vouchers.clear()
self.common_filter.clear()
self.min_outstanding = self.max_outstanding = None
# clear result
self.voucher_outstandings.clear()
def query_for_outstanding(self):
"""
Database query to fetch voucher amount and voucher outstanding using Common Table Expression
"""
ple = self.ple
filter_on_voucher_no = []
filter_on_against_voucher_no = []
if self.vouchers:
voucher_types = set([x.voucher_type for x in self.vouchers])
voucher_nos = set([x.voucher_no for x in self.vouchers])
filter_on_voucher_no.append(ple.voucher_type.isin(voucher_types))
filter_on_voucher_no.append(ple.voucher_no.isin(voucher_nos))
filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
# build outstanding amount filter
filter_on_outstanding_amount = []
if self.min_outstanding:
if self.min_outstanding > 0:
filter_on_outstanding_amount.append(
Table("outstanding").amount_in_account_currency >= self.min_outstanding
)
else:
filter_on_outstanding_amount.append(
Table("outstanding").amount_in_account_currency <= self.min_outstanding
)
if self.max_outstanding:
if self.max_outstanding > 0:
filter_on_outstanding_amount.append(
Table("outstanding").amount_in_account_currency <= self.max_outstanding
)
else:
filter_on_outstanding_amount.append(
Table("outstanding").amount_in_account_currency >= self.max_outstanding
)
# build query for voucher amount
query_voucher_amount = (
qb.from_(ple)
.select(
ple.account,
ple.voucher_type,
ple.voucher_no,
ple.party_type,
ple.party,
ple.posting_date,
ple.due_date,
ple.account_currency.as_("currency"),
Sum(ple.amount).as_("amount"),
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
)
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_voucher_no))
.where(Criterion.all(self.common_filter))
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
)
# build query for voucher outstanding
query_voucher_outstanding = (
qb.from_(ple)
.select(
ple.account,
ple.against_voucher_type.as_("voucher_type"),
ple.against_voucher_no.as_("voucher_no"),
ple.party_type,
ple.party,
ple.posting_date,
ple.due_date,
ple.account_currency.as_("currency"),
Sum(ple.amount).as_("amount"),
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
)
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_against_voucher_no))
.where(Criterion.all(self.common_filter))
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
)
# build CTE for combining voucher amount and outstanding
self.cte_query_voucher_amount_and_outstanding = (
qb.with_(query_voucher_amount, "vouchers")
.with_(query_voucher_outstanding, "outstanding")
.from_(AliasedQuery("vouchers"))
.left_join(AliasedQuery("outstanding"))
.on(
(AliasedQuery("vouchers").account == AliasedQuery("outstanding").account)
& (AliasedQuery("vouchers").voucher_type == AliasedQuery("outstanding").voucher_type)
& (AliasedQuery("vouchers").voucher_no == AliasedQuery("outstanding").voucher_no)
& (AliasedQuery("vouchers").party_type == AliasedQuery("outstanding").party_type)
& (AliasedQuery("vouchers").party == AliasedQuery("outstanding").party)
)
.select(
Table("vouchers").account,
Table("vouchers").voucher_type,
Table("vouchers").voucher_no,
Table("vouchers").party_type,
Table("vouchers").party,
Table("vouchers").posting_date,
Table("vouchers").amount.as_("invoice_amount"),
Table("vouchers").amount_in_account_currency.as_("invoice_amount_in_account_currency"),
Table("outstanding").amount.as_("outstanding"),
Table("outstanding").amount_in_account_currency.as_("outstanding_in_account_currency"),
(Table("vouchers").amount - Table("outstanding").amount).as_("paid_amount"),
(
Table("vouchers").amount_in_account_currency - Table("outstanding").amount_in_account_currency
).as_("paid_amount_in_account_currency"),
Table("vouchers").due_date,
Table("vouchers").currency,
)
.where(Criterion.all(filter_on_outstanding_amount))
)
# build CTE filter
# only fetch invoices
if self.get_invoices:
self.cte_query_voucher_amount_and_outstanding = (
self.cte_query_voucher_amount_and_outstanding.having(
qb.Field("outstanding_in_account_currency") > 0
)
)
# only fetch payments
elif self.get_payments:
self.cte_query_voucher_amount_and_outstanding = (
self.cte_query_voucher_amount_and_outstanding.having(
qb.Field("outstanding_in_account_currency") < 0
)
)
# execute SQL
self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
def get_voucher_outstandings(
self,
vouchers=None,
common_filter=None,
min_outstanding=None,
max_outstanding=None,
get_payments=False,
get_invoices=False,
):
"""
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
vouchers - dict of vouchers to get
common_filter - array of criterions
min_outstanding - filter on minimum total outstanding amount
max_outstanding - filter on maximum total outstanding amount
get_invoices - only fetch vouchers(ledger entries with +ve outstanding)
get_payments - only fetch payments(ledger entries with -ve outstanding)
"""
self.reset()
self.vouchers = vouchers
self.common_filter = common_filter or []
self.min_outstanding = min_outstanding
self.max_outstanding = max_outstanding
self.get_payments = get_payments
self.get_invoices = get_invoices
self.query_for_outstanding()
return self.voucher_outstandings

View File

@ -504,18 +504,6 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "DATEV Export",
"link_count": 0,
"link_to": "DATEV",
"link_type": "Report",
"onboard": 0,
"only_for": "Germany",
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
@ -1024,16 +1012,16 @@
"type": "Link"
},
{
"dependencies": "Cost Center",
"hidden": 0,
"is_query_report": 0,
"label": "Cost Center Allocation",
"link_count": 0,
"link_to": "Cost Center Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
"dependencies": "Cost Center",
"hidden": 0,
"is_query_report": 0,
"label": "Cost Center Allocation",
"link_count": 0,
"link_to": "Cost Center Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Cost Center",
"hidden": 0,
@ -1235,13 +1223,14 @@
"type": "Link"
}
],
"modified": "2022-01-13 17:25:09.835345",
"modified": "2022-06-10 15:49:42.990860",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 2.0,

View File

@ -47,17 +47,19 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
team_member = frappe.db.get_value("User", assign_to_member, "email")
args = {
"doctype": "Asset Maintenance",
"assign_to": [team_member],
"assign_to": team_member,
"name": asset_maintenance_name,
"description": maintenance_task,
"date": next_due_date,
}
if not frappe.db.sql(
"""select owner from `tabToDo`
where reference_type=%(doctype)s and reference_name=%(name)s and status="Open"
where reference_type=%(doctype)s and reference_name=%(name)s and status='Open'
and owner=%(assign_to)s""",
args,
):
# assign_to function expects a list
args["assign_to"] = [args["assign_to"]]
assign_to.add(args)

View File

@ -330,7 +330,7 @@ class TestPurchaseOrder(FrappeTestCase):
else:
# update valid from
frappe.db.sql(
"""UPDATE `tabItem Tax` set valid_from = CURDATE()
"""UPDATE `tabItem Tax` set valid_from = CURRENT_DATE
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template},
)

View File

@ -213,6 +213,7 @@
"width": "60px"
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
@ -242,6 +243,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "UOM Conversion Factor",
@ -593,6 +595,7 @@
"label": "Billed, Received & Returned"
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Qty in Stock UOM",
@ -851,7 +854,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-02-02 13:10:18.398976",
"modified": "2022-06-17 05:29:40.602349",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@ -285,7 +285,7 @@ def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters):
"""select `tabContact`.name from `tabContact`, `tabDynamic Link`
where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s
and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent
limit %(start)s, %(page_len)s""",
limit %(page_len)s offset %(start)s""",
{"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")},
)

View File

@ -84,6 +84,9 @@ class Supplier(TransactionBase):
self.save()
def validate_internal_supplier(self):
if not self.is_internal_supplier:
self.represents_company = ""
internal_supplier = frappe.db.get_value(
"Supplier",
{

View File

@ -252,7 +252,7 @@ def get_mapped_pi_records():
ON pi_item.`purchase_order` = po.`name`
WHERE
pi_item.docstatus = 1
AND po.status not in ("Closed","Completed","Cancelled")
AND po.status not in ('Closed','Completed','Cancelled')
AND pi_item.po_detail IS NOT NULL
"""
)
@ -271,7 +271,7 @@ def get_mapped_pr_records():
pr.docstatus=1
AND pr.name=pr_item.parent
AND pr_item.purchase_order_item IS NOT NULL
AND pr.status not in ("Closed","Completed","Cancelled")
AND pr.status not in ('Closed','Completed','Cancelled')
"""
)
)
@ -302,7 +302,7 @@ def get_po_entries(conditions):
WHERE
parent.docstatus = 1
AND parent.name = child.parent
AND parent.status not in ("Closed","Completed","Cancelled")
AND parent.status not in ('Closed','Completed','Cancelled')
{conditions}
GROUP BY
parent.name, child.item_code

View File

@ -46,6 +46,7 @@ from erpnext.controllers.print_settings import (
from erpnext.controllers.sales_and_purchase_return import validate_return
from erpnext.exceptions import InvalidCurrency
from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import (
_get_item_tax_template,
@ -548,6 +549,15 @@ class AccountsController(TransactionBase):
if ret.get("pricing_rules"):
self.apply_pricing_rule_on_items(item, ret)
self.set_pricing_rule_details(item, ret)
else:
# Transactions line item without item code
uom = item.get("uom")
stock_uom = item.get("stock_uom")
if bool(uom) != bool(stock_uom): # xor
item.stock_uom = item.uom = uom or stock_uom
item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
if self.doctype == "Purchase Invoice":
self.set_expense_account(for_validate)
@ -1838,6 +1848,17 @@ class AccountsController(TransactionBase):
jv.save()
jv.submit()
def check_conversion_rate(self):
default_currency = erpnext.get_company_currency(self.company)
if not default_currency:
throw(_("Please enter default currency in Company Master"))
if (
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
or not self.conversion_rate
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
):
throw(_("Conversion rate cannot be 0 or 1"))
@frappe.whitelist()
def get_tax_rate(account_head):
@ -2039,7 +2060,7 @@ def get_advance_journal_entries(
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as reference_type, t1.name as reference_name,
'Journal Entry' as reference_type, t1.name as reference_name,
t1.remark as remarks, t2.{0} as amount, t2.name as reference_row,
t2.reference_name as against_order, t2.exchange_rate
from
@ -2094,7 +2115,7 @@ def get_advance_payment_entries(
payment_entries_against_order = frappe.db.sql(
"""
select
"Payment Entry" as reference_type, t1.name as reference_name,
'Payment Entry' as reference_type, t1.name as reference_name,
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
t2.reference_name as against_order, t1.posting_date,
t1.{0} as currency, t1.{4} as exchange_rate
@ -2114,7 +2135,7 @@ def get_advance_payment_entries(
if include_unallocated:
unallocated_payment_entries = frappe.db.sql(
"""
select "Payment Entry" as reference_type, name as reference_name, posting_date,
select 'Payment Entry' as reference_type, name as reference_name, posting_date,
remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency
from `tabPayment Entry`
where

View File

@ -29,11 +29,11 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
or employee_name like %(txt)s)
{fcond} {mcond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
(case when locate(%(_txt)s, employee_name) > 0 then locate(%(_txt)s, employee_name) else 99999 end),
idx desc,
name, employee_name
limit %(start)s, %(page_len)s""".format(
limit %(page_len)s offset %(start)s""".format(
**{
"fields": ", ".join(fields),
"key": searchfield,
@ -60,12 +60,12 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
or company_name like %(txt)s)
{mcond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
if(locate(%(_txt)s, lead_name), locate(%(_txt)s, lead_name), 99999),
if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999),
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
(case when locate(%(_txt)s, lead_name) > 0 then locate(%(_txt)s, lead_name) else 99999 end),
(case when locate(%(_txt)s, company_name) > 0 then locate(%(_txt)s, company_name) else 99999 end),
idx desc,
name, lead_name
limit %(start)s, %(page_len)s""".format(
limit %(page_len)s offset %(start)s""".format(
**{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
@ -96,11 +96,11 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
and ({scond}) and disabled=0
{fcond} {mcond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999),
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
(case when locate(%(_txt)s, customer_name) > 0 then locate(%(_txt)s, customer_name) else 99999 end),
idx desc,
name, customer_name
limit %(start)s, %(page_len)s""".format(
limit %(page_len)s offset %(start)s""".format(
**{
"fields": ", ".join(fields),
"scond": searchfields,
@ -130,14 +130,14 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
where docstatus < 2
and ({key} like %(txt)s
or supplier_name like %(txt)s) and disabled=0
and (on_hold = 0 or (on_hold = 1 and CURDATE() > release_date))
and (on_hold = 0 or (on_hold = 1 and CURRENT_DATE > release_date))
{mcond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
(case when locate(%(_txt)s, supplier_name) > 0 then locate(%(_txt)s, supplier_name) else 99999 end),
idx desc,
name, supplier_name
limit %(start)s, %(page_len)s """.format(
limit %(page_len)s offset %(start)s""".format(
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
@ -167,7 +167,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
AND `{searchfield}` LIKE %(txt)s
{mcond}
ORDER BY idx DESC, name
LIMIT %(offset)s, %(limit)s
LIMIT %(limit)s offset %(offset)s
""".format(
account_type_condition=account_type_condition,
searchfield=searchfield,
@ -305,15 +305,15 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
"""select {fields}
from tabBOM
where tabBOM.docstatus=1
and tabBOM.is_active=1
and tabBOM.`{key}` like %(txt)s
from `tabBOM`
where `tabBOM`.docstatus=1
and `tabBOM`.is_active=1
and `tabBOM`.`{key}` like %(txt)s
{fcond} {mcond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
idx desc, name
limit %(start)s, %(page_len)s """.format(
limit %(page_len)s offset %(start)s""".format(
fields=", ".join(fields),
fcond=get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
mcond=get_match_cond(doctype).replace("%", "%%"),
@ -340,18 +340,18 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
fields = get_fields("Project", ["name", "project_name"])
searchfields = frappe.get_meta("Project").get_search_fields()
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
return frappe.db.sql(
"""select {fields} from `tabProject`
where
`tabProject`.status not in ("Completed", "Cancelled")
`tabProject`.status not in ('Completed', 'Cancelled')
and {cond} {scond} {match_cond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc,
(case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end),
`tabProject`.idx desc,
`tabProject`.name asc
limit {start}, {page_len}""".format(
limit {page_len} offset {start}""".format(
fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
cond=cond,
scond=searchfields,
@ -374,7 +374,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
from `tabDelivery Note`
where `tabDelivery Note`.`%(key)s` like %(txt)s and
`tabDelivery Note`.docstatus = 1
and status not in ("Stopped", "Closed") %(fcond)s
and status not in ('Stopped', 'Closed') %(fcond)s
and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
@ -383,7 +383,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
)
)
%(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s
%(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(page_len)s offset %(start)s
"""
% {
"fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]),
@ -456,7 +456,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
{match_conditions}
group by batch_no {having_clause}
order by batch.expiry_date, sle.batch_no desc
limit %(start)s, %(page_len)s""".format(
limit %(page_len)s offset %(start)s""".format(
search_columns=search_columns,
cond=cond,
match_conditions=get_match_cond(doctype),
@ -483,7 +483,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
{match_conditions}
order by expiry_date, name desc
limit %(start)s, %(page_len)s""".format(
limit %(page_len)s offset %(start)s""".format(
cond,
search_columns=search_columns,
search_cond=search_cond,
@ -654,7 +654,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
filter_dict = get_doctype_wise_filters(filters)
query = """select `tabWarehouse`.name,
CONCAT_WS(" : ", "Actual Qty", ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
from `tabWarehouse` left join `tabBin`
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
where
@ -662,7 +662,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
{fcond} {mcond}
order by ifnull(`tabBin`.actual_qty, 0) desc
limit
{start}, {page_len}
{page_len} offset {start}
""".format(
bin_conditions=get_filters_cond(
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
@ -691,7 +691,7 @@ def get_doctype_wise_filters(filters):
def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters):
query = """select batch_id from `tabBatch`
where disabled = 0
and (expiry_date >= CURDATE() or expiry_date IS NULL)
and (expiry_date >= CURRENT_DATE or expiry_date IS NULL)
and name like {txt}""".format(
txt=frappe.db.escape("%{0}%".format(txt))
)

View File

@ -35,7 +35,8 @@ status_map = {
["Draft", None],
["Open", "eval:self.docstatus==1"],
["Lost", "eval:self.status=='Lost'"],
["Ordered", "has_sales_order"],
["Partially Ordered", "is_partially_ordered"],
["Ordered", "is_fully_ordered"],
["Cancelled", "eval:self.docstatus==2"],
],
"Sales Order": [
@ -351,9 +352,9 @@ class StatusUpdater(Document):
for args in self.status_updater:
# condition to include current record (if submit or no if cancel)
if self.docstatus == 1:
args["cond"] = ' or parent="%s"' % self.name.replace('"', '"')
args["cond"] = " or parent='%s'" % self.name.replace('"', '"')
else:
args["cond"] = ' and parent!="%s"' % self.name.replace('"', '"')
args["cond"] = " and parent!='%s'" % self.name.replace('"', '"')
self._update_children(args, update_modified)
@ -383,7 +384,7 @@ class StatusUpdater(Document):
args["second_source_condition"] = frappe.db.sql(
""" select ifnull((select sum(%(second_source_field)s)
from `tab%(second_source_dt)s`
where `%(second_join_field)s`="%(detail_id)s"
where `%(second_join_field)s`='%(detail_id)s'
and (`tab%(second_source_dt)s`.docstatus=1)
%(second_source_extra_cond)s), 0) """
% args
@ -397,7 +398,7 @@ class StatusUpdater(Document):
frappe.db.sql(
"""
(select ifnull(sum(%(source_field)s), 0)
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
from `tab%(source_dt)s` where `%(join_field)s`='%(detail_id)s'
and (docstatus=1 %(cond)s) %(extra_cond)s)
"""
% args
@ -442,9 +443,9 @@ class StatusUpdater(Document):
"""update `tab%(target_parent_dt)s`
set %(target_parent_field)s = round(
ifnull((select
ifnull(sum(if(abs(%(target_ref_field)s) > abs(%(target_field)s), abs(%(target_field)s), abs(%(target_ref_field)s))), 0)
ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0)
/ sum(abs(%(target_ref_field)s)) * 100
from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
from `tab%(target_dt)s` where parent='%(name)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
%(update_modified)s
where name='%(name)s'"""
% args
@ -454,9 +455,9 @@ class StatusUpdater(Document):
if args.get("status_field"):
frappe.db.sql(
"""update `tab%(target_parent_dt)s`
set %(status_field)s = if(%(target_parent_field)s<0.001,
'Not %(keyword)s', if(%(target_parent_field)s>=99.999999,
'Fully %(keyword)s', 'Partly %(keyword)s'))
set %(status_field)s = (case when %(target_parent_field)s<0.001 then 'Not %(keyword)s'
else case when %(target_parent_field)s>=99.999999 then 'Fully %(keyword)s'
else 'Partly %(keyword)s' end end)
where name='%(name)s'"""
% args
)

View File

@ -8,7 +8,8 @@ import frappe
from frappe import _
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType
from frappe.query_builder import DocType, Interval
from frappe.query_builder.functions import Now
from frappe.utils import cint, flt, get_fullname
from erpnext.crm.utils import add_link_in_communication, copy_comments
@ -398,15 +399,17 @@ def auto_close_opportunity():
frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15
)
opportunities = frappe.db.sql(
""" select name from tabOpportunity where status='Replied' and
modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """,
(auto_close_after_days),
as_dict=True,
)
table = frappe.qb.DocType("Opportunity")
opportunities = (
frappe.qb.from_(table)
.select(table.name)
.where(
(table.modified < (Now() - Interval(days=auto_close_after_days))) & (table.status == "Replied")
)
).run(pluck=True)
for opportunity in opportunities:
doc = frappe.get_doc("Opportunity", opportunity.get("name"))
doc = frappe.get_doc("Opportunity", opportunity)
doc.status = "Closed"
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True

View File

@ -1,44 +0,0 @@
import frappe
from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection
from github import Github
class GithubConnection(BaseConnection):
def __init__(self, connector):
self.connector = connector
try:
password = self.get_password()
except frappe.AuthenticationError:
password = None
if self.connector.username and password:
self.connection = Github(self.connector.username, self.get_password())
else:
self.connection = Github()
self.name_field = 'id'
def insert(self, doctype, doc):
pass
def update(self, doctype, doc, migration_id):
pass
def delete(self, doctype, migration_id):
pass
def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10):
repo = filters.get('repo')
if remote_objectname == 'Milestone':
return self.get_milestones(repo, start, page_length)
if remote_objectname == 'Issue':
return self.get_issues(repo, start, page_length)
def get_milestones(self, repo, start=0, page_length=10):
_repo = self.connection.get_repo(repo)
return list(_repo.get_milestones()[start:start+page_length])
def get_issues(self, repo, start=0, page_length=10):
_repo = self.connection.get_repo(repo)
return list(_repo.get_issues()[start:start+page_length])

View File

@ -1,12 +0,0 @@
import frappe
def pre_process(issue):
project = frappe.db.get_value("Project", filters={"project_name": issue.milestone})
return {
"title": issue.title,
"body": frappe.utils.md_to_html(issue.body or ""),
"state": issue.state.title(),
"project": project or "",
}

View File

@ -1,36 +0,0 @@
{
"condition": "{\"repo\":\"frappe/erpnext\"}",
"creation": "2017-10-16 16:03:32.772191",
"docstatus": 0,
"doctype": "Data Migration Mapping",
"fields": [
{
"is_child_table": 0,
"local_fieldname": "subject",
"remote_fieldname": "title"
},
{
"is_child_table": 0,
"local_fieldname": "description",
"remote_fieldname": "body"
},
{
"is_child_table": 0,
"local_fieldname": "status",
"remote_fieldname": "state"
}
],
"idx": 0,
"local_doctype": "Task",
"local_primary_key": "name",
"mapping_name": "Issue to Task",
"mapping_type": "Pull",
"migration_id_field": "github_sync_id",
"modified": "2017-10-20 11:48:54.575993",
"modified_by": "Administrator",
"name": "Issue to Task",
"owner": "Administrator",
"page_length": 10,
"remote_objectname": "Issue",
"remote_primary_key": "id"
}

View File

@ -1,6 +0,0 @@
def pre_process(milestone):
return {
"title": milestone.title,
"description": milestone.description,
"state": milestone.state.title(),
}

View File

@ -1,36 +0,0 @@
{
"condition": "{\"repo\": \"frappe/erpnext\"}",
"creation": "2017-10-13 11:16:49.664925",
"docstatus": 0,
"doctype": "Data Migration Mapping",
"fields": [
{
"is_child_table": 0,
"local_fieldname": "project_name",
"remote_fieldname": "title"
},
{
"is_child_table": 0,
"local_fieldname": "notes",
"remote_fieldname": "description"
},
{
"is_child_table": 0,
"local_fieldname": "status",
"remote_fieldname": "state"
}
],
"idx": 0,
"local_doctype": "Project",
"local_primary_key": "project_name",
"mapping_name": "Milestone to Project",
"mapping_type": "Pull",
"migration_id_field": "github_sync_id",
"modified": "2017-10-20 11:48:54.552305",
"modified_by": "Administrator",
"name": "Milestone to Project",
"owner": "Administrator",
"page_length": 10,
"remote_objectname": "Milestone",
"remote_primary_key": "id"
}

View File

@ -1,22 +0,0 @@
{
"creation": "2017-10-13 11:16:53.600026",
"docstatus": 0,
"doctype": "Data Migration Plan",
"idx": 0,
"mappings": [
{
"enabled": 1,
"mapping": "Milestone to Project"
},
{
"enabled": 1,
"mapping": "Issue to Task"
}
],
"modified": "2017-10-20 11:48:54.496123",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "GitHub Sync",
"owner": "Administrator",
"plan_name": "GitHub Sync"
}

View File

@ -23,7 +23,7 @@ class TestMpesaSettings(unittest.TestCase):
def tearDown(self):
frappe.db.sql("delete from `tabMpesa Settings`")
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
def test_creation_of_payment_gateway(self):
mode_of_payment = create_mode_of_payment("Mpesa-_Test", payment_type="Phone")

View File

@ -392,9 +392,12 @@ after_migrate = ["erpnext.setup.install.update_select_perm_after_install"]
scheduler_events = {
"cron": {
"0/5 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",
]
],
},
"all": [
"erpnext.projects.doctype.project.project.project_status_update_reminder",

View File

@ -827,7 +827,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2022-04-22 16:21:55.811983",
"modified": "2022-06-10 01:29:32.952091",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
@ -872,7 +872,6 @@
],
"search_fields": "employee_name",
"show_name_in_global_search": 1,
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],

View File

@ -216,7 +216,7 @@ def make_payment_entry(advance):
def make_employee_advance(employee_name, args=None):
doc = frappe.new_doc("Employee Advance")
doc.employee = employee_name
doc.company = "_Test company"
doc.company = "_Test Company"
doc.purpose = "For site visit"
doc.currency = erpnext.get_company_currency("_Test company")
doc.exchange_rate = 1

View File

@ -88,7 +88,7 @@ def send_exit_questionnaire(interviews):
reference_doctype=interview.doctype,
reference_name=interview.name,
)
interview.db_set("questionnaire_email_sent", True)
interview.db_set("questionnaire_email_sent", 1)
interview.notify_update()
email_success.append(email)
else:

View File

@ -49,7 +49,7 @@ class TestJobOffer(unittest.TestCase):
frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
def tearDown(self):
frappe.db.sql("DELETE FROM `tabJob Offer` WHERE 1")
frappe.db.sql("DELETE FROM `tabJob Offer`")
def create_job_offer(**args):

View File

@ -399,7 +399,7 @@ class LeaveApplication(Document):
select
name, leave_type, posting_date, from_date, to_date, total_leave_days, half_day_date
from `tabLeave Application`
where employee = %(employee)s and docstatus < 2 and status in ("Open", "Approved")
where employee = %(employee)s and docstatus < 2 and status in ('Open', 'Approved')
and to_date >= %(from_date)s and from_date <= %(to_date)s
and name != %(name)s""",
{
@ -439,7 +439,7 @@ class LeaveApplication(Document):
"""select count(name) from `tabLeave Application`
where employee = %(employee)s
and docstatus < 2
and status in ("Open", "Approved")
and status in ('Open', 'Approved')
and half_day = 1
and half_day_date = %(half_day_date)s
and name != %(name)s""",
@ -456,7 +456,7 @@ class LeaveApplication(Document):
def validate_attendance(self):
attendance = frappe.db.sql(
"""select name from `tabAttendance` where employee = %s and (attendance_date between %s and %s)
and status = "Present" and docstatus = 1""",
and status = 'Present' and docstatus = 1""",
(self.employee, self.from_date, self.to_date),
)
if attendance:

View File

@ -108,7 +108,7 @@ class TestLeaveApplication(unittest.TestCase):
def _clear_roles(self):
frappe.db.sql(
"""delete from `tabHas Role` where parent in
("test@example.com", "test1@example.com", "test2@example.com")"""
('test@example.com', 'test1@example.com', 'test2@example.com')"""
)
def _clear_applications(self):

View File

@ -5,6 +5,7 @@ import frappe
from frappe import _
from frappe.query_builder import Order
from frappe.utils import getdate
from pypika import functions as fn
def execute(filters=None):
@ -110,7 +111,7 @@ def get_data(filters):
)
.distinct()
.where(
((employee.relieving_date.isnotnull()) | (employee.relieving_date != ""))
(fn.Coalesce(fn.Cast(employee.relieving_date, "char"), "") != "")
& ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2)))
& ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2)))
)

View File

@ -20,7 +20,7 @@ class TestVehicleExpenses(unittest.TestCase):
frappe.db.sql("delete from `tabVehicle Log`")
employee_id = frappe.db.sql(
'''select name from `tabEmployee` where name="testdriver@example.com"'''
"""select name from `tabEmployee` where name='testdriver@example.com' """
)
self.employee_id = employee_id[0][0] if employee_id else None
if not self.employee_id:

View File

@ -55,6 +55,8 @@ def update_employee_work_history(employee, details, date=None, cancel=False):
new_data = getdate(new_data)
elif fieldtype == "Datetime" and new_data:
new_data = get_datetime(new_data)
elif fieldtype in ["Currency", "Float"] and new_data:
new_data = flt(new_data)
setattr(employee, item.fieldname, new_data)
if item.fieldname in ["department", "designation", "branch"]:
internal_work_history[item.fieldname] = item.new
@ -456,7 +458,7 @@ def get_salary_assignments(employee, payroll_period):
def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
total_given_benefit_amount = 0
query = """
select sum(sd.amount) as 'total_amount'
select sum(sd.amount) as total_amount
from `tabSalary Slip` ss, `tabSalary Detail` sd
where ss.employee=%(employee)s
and ss.docstatus = 1 and ss.name = sd.parent

View File

@ -73,7 +73,7 @@ class Loan(AccountsController):
def on_cancel(self):
self.unlink_loan_security_pledge()
self.ignore_linked_doctypes = ["GL Entry"]
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def set_missing_fields(self):
if not self.company:

View File

@ -29,7 +29,7 @@ class LoanDisbursement(AccountsController):
def on_cancel(self):
self.set_status_and_amounts(cancel=1)
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ["GL Entry"]
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def set_missing_values(self):
if not self.disbursement_date:

View File

@ -32,7 +32,7 @@ class LoanInterestAccrual(AccountsController):
self.update_is_accrued()
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ["GL Entry"]
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def update_is_accrued(self):
frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)

View File

@ -41,7 +41,7 @@ class LoanRepayment(AccountsController):
self.check_future_accruals()
self.update_repayment_schedule(cancel=1)
self.mark_as_unpaid()
self.ignore_linked_doctypes = ["GL Entry"]
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1)
def set_missing_values(self, amounts):

View File

@ -42,7 +42,7 @@ class LoanWriteOff(AccountsController):
def on_cancel(self):
self.update_outstanding_amount(cancel=1)
self.ignore_linked_doctypes = ["GL Entry"]
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1)
def update_outstanding_amount(self, cancel=0):

View File

@ -81,7 +81,7 @@ frappe.ui.form.on("BOM", {
}
)
if (!frm.doc.__islocal && frm.doc.docstatus<2) {
if (!frm.is_new() && frm.doc.docstatus<2) {
frm.add_custom_button(__("Update Cost"), function() {
frm.events.update_cost(frm, true);
});
@ -93,10 +93,12 @@ frappe.ui.form.on("BOM", {
});
}
frm.add_custom_button(__("New Version"), function() {
let new_bom = frappe.model.copy_doc(frm.doc);
frappe.set_route("Form", "BOM", new_bom.name);
});
if (!frm.is_new() && !frm.doc.docstatus == 0) {
frm.add_custom_button(__("New Version"), function() {
let new_bom = frappe.model.copy_doc(frm.doc);
frappe.set_route("Form", "BOM", new_bom.name);
});
}
if(frm.doc.docstatus==1) {
frm.add_custom_button(__("Work Order"), function() {

View File

@ -1,11 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import functools
import re
from collections import deque
from operator import itemgetter
from typing import List
from typing import Dict, List
import frappe
from frappe import _
@ -189,6 +189,7 @@ class BOM(WebsiteGenerator):
self.validate_transfer_against()
self.set_routing_operations()
self.validate_operations()
self.update_exploded_items(save=False)
self.calculate_cost()
self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
@ -386,40 +387,14 @@ class BOM(WebsiteGenerator):
existing_bom_cost = self.total_cost
for d in self.get("items"):
if not d.item_code:
continue
rate = self.get_rm_rate(
{
"company": self.company,
"item_code": d.item_code,
"bom_no": d.bom_no,
"qty": d.qty,
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
"sourced_by_supplier": d.sourced_by_supplier,
}
)
if rate:
d.rate = rate
d.amount = flt(d.rate) * flt(d.qty)
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.base_amount = flt(d.amount) * flt(self.conversion_rate)
if save:
d.db_update()
if self.docstatus == 1:
self.flags.ignore_validate_update_after_submit = True
self.calculate_cost(update_hour_rate)
self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate)
if save:
self.db_update()
self.update_exploded_items(save=save)
# update parent BOMs
if self.total_cost != existing_bom_cost and update_parent:
parent_boms = frappe.db.sql_list(
@ -608,11 +583,15 @@ class BOM(WebsiteGenerator):
bom_list.reverse()
return bom_list
def calculate_cost(self, update_hour_rate=False):
def calculate_cost(self, save_updates=False, update_hour_rate=False):
"""Calculate bom totals"""
self.calculate_op_cost(update_hour_rate)
self.calculate_rm_cost()
self.calculate_sm_cost()
self.calculate_rm_cost(save=save_updates)
self.calculate_sm_cost(save=save_updates)
if save_updates:
# not via doc event, table is not regenerated and needs updation
self.calculate_exploded_cost()
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
self.base_total_cost = (
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
@ -654,12 +633,26 @@ class BOM(WebsiteGenerator):
if update_hour_rate:
row.db_update()
def calculate_rm_cost(self):
def calculate_rm_cost(self, save=False):
"""Fetch RM rate as per today's valuation rate and calculate totals"""
total_rm_cost = 0
base_total_rm_cost = 0
for d in self.get("items"):
old_rate = d.rate
d.rate = self.get_rm_rate(
{
"company": self.company,
"item_code": d.item_code,
"bom_no": d.bom_no,
"qty": d.qty,
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
"sourced_by_supplier": d.sourced_by_supplier,
}
)
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
d.base_amount = d.amount * flt(self.conversion_rate)
@ -669,11 +662,13 @@ class BOM(WebsiteGenerator):
total_rm_cost += d.amount
base_total_rm_cost += d.base_amount
if save and (old_rate != d.rate):
d.db_update()
self.raw_material_cost = total_rm_cost
self.base_raw_material_cost = base_total_rm_cost
def calculate_sm_cost(self):
def calculate_sm_cost(self, save=False):
"""Fetch RM rate as per today's valuation rate and calculate totals"""
total_sm_cost = 0
base_total_sm_cost = 0
@ -688,10 +683,45 @@ class BOM(WebsiteGenerator):
)
total_sm_cost += d.amount
base_total_sm_cost += d.base_amount
if save:
d.db_update()
self.scrap_material_cost = total_sm_cost
self.base_scrap_material_cost = base_total_sm_cost
def calculate_exploded_cost(self):
"Set exploded row cost from it's parent BOM."
rm_rate_map = self.get_rm_rate_map()
for row in self.get("exploded_items"):
old_rate = flt(row.rate)
row.rate = rm_rate_map.get(row.item_code)
row.amount = flt(row.stock_qty) * flt(row.rate)
if old_rate != row.rate:
# Only db_update if changed
row.db_update()
def get_rm_rate_map(self) -> Dict[str, float]:
"Create Raw Material-Rate map for Exploded Items. Fetch rate from Items table or Subassembly BOM."
rm_rate_map = {}
for item in self.get("items"):
if item.bom_no:
# Get Item-Rate from Subassembly BOM
explosion_items = frappe.get_all(
"BOM Explosion Item",
filters={"parent": item.bom_no},
fields=["item_code", "rate"],
order_by=None, # to avoid sort index creation at db level (granular change)
)
explosion_item_rate = {item.item_code: flt(item.rate) for item in explosion_items}
rm_rate_map.update(explosion_item_rate)
else:
rm_rate_map[item.item_code] = flt(item.base_rate) / flt(item.conversion_factor or 1.0)
return rm_rate_map
def update_exploded_items(self, save=True):
"""Update Flat BOM, following will be correct data"""
self.get_exploded_items()
@ -902,44 +932,46 @@ def get_bom_item_rate(args, bom_doc):
return flt(rate)
def get_valuation_rate(args):
"""Get weighted average of valuation rate from all warehouses"""
def get_valuation_rate(data):
"""
1) Get average valuation rate from all warehouses
2) If no value, get last valuation rate from SLE
3) If no value, get valuation rate from Item
"""
from frappe.query_builder.functions import Sum
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
item_bins = frappe.db.sql(
"""
select
bin.actual_qty, bin.stock_value
from
`tabBin` bin, `tabWarehouse` warehouse
where
bin.item_code=%(item)s
and bin.warehouse = warehouse.name
and warehouse.company=%(company)s""",
{"item": args["item_code"], "company": args["company"]},
as_dict=1,
)
item_code, company = data.get("item_code"), data.get("company")
valuation_rate = 0.0
for d in item_bins:
total_qty += flt(d.actual_qty)
total_value += flt(d.stock_value)
bin_table = frappe.qb.DocType("Bin")
wh_table = frappe.qb.DocType("Warehouse")
item_valuation = (
frappe.qb.from_(bin_table)
.join(wh_table)
.on(bin_table.warehouse == wh_table.name)
.select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
.where((bin_table.item_code == item_code) & (wh_table.company == company))
).run(as_dict=True)[0]
if total_qty:
valuation_rate = total_value / total_qty
valuation_rate = item_valuation.get("valuation_rate")
if valuation_rate <= 0:
last_valuation_rate = frappe.db.sql(
"""select valuation_rate
from `tabStock Ledger Entry`
where item_code = %s and valuation_rate > 0 and is_cancelled = 0
order by posting_date desc, posting_time desc, creation desc limit 1""",
args["item_code"],
)
if (valuation_rate is not None) and valuation_rate <= 0:
# Explicit null value check. If None, Bins don't exist, neither does SLE
sle = frappe.qb.DocType("Stock Ledger Entry")
last_val_rate = (
frappe.qb.from_(sle)
.select(sle.valuation_rate)
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
.orderby(sle.posting_date, order=frappe.qb.desc)
.orderby(sle.posting_time, order=frappe.qb.desc)
.orderby(sle.creation, order=frappe.qb.desc)
.limit(1)
).run(as_dict=True)
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
valuation_rate = flt(last_val_rate[0].get("valuation_rate")) if last_val_rate else 0
if not valuation_rate:
valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate")
valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
return flt(valuation_rate)
@ -1125,39 +1157,6 @@ def get_children(parent=None, is_root=False, **filters):
return bom_items
def get_boms_in_bottom_up_order(bom_no=None):
def _get_parent(bom_no):
return frappe.db.sql_list(
"""
select distinct bom_item.parent from `tabBOM Item` bom_item
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
""",
bom_no,
)
count = 0
bom_list = []
if bom_no:
bom_list.append(bom_no)
else:
# get all leaf BOMs
bom_list = frappe.db.sql_list(
"""select name from `tabBOM` bom
where docstatus=1 and is_active=1
and not exists(select bom_no from `tabBOM Item`
where parent=bom.name and ifnull(bom_no, '')!='')"""
)
while count < len(bom_list):
for child_bom in _get_parent(bom_list[count]):
if child_bom not in bom_list:
bom_list.append(child_bom)
count += 1
return bom_list
def add_additional_cost(stock_entry, work_order):
# Add non stock items cost in the additional cost
stock_entry.additional_costs = []
@ -1306,7 +1305,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not field in searchfields
]
query_filters = {"disabled": 0, "ifnull(end_of_life, '5050-50-50')": (">", today())}
query_filters = {"disabled": 0, "end_of_life": (">", today())}
or_cond_filters = {}
if txt:

View File

@ -11,7 +11,9 @@ from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
update_cost_in_all_boms_in_test,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
@ -69,26 +71,31 @@ class TestBOM(FrappeTestCase):
def test_update_bom_cost_in_all_boms(self):
# get current rate for '_Test Item 2'
rm_rate = frappe.db.sql(
"""select rate from `tabBOM Item`
where parent='BOM-_Test Item Home Desktop Manufactured-001'
and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'"""
bom_rates = frappe.db.get_values(
"BOM Item",
{
"parent": "BOM-_Test Item Home Desktop Manufactured-001",
"item_code": "_Test Item 2",
"docstatus": 1,
},
fieldname=["rate", "base_rate"],
as_dict=True,
)
rm_rate = rm_rate[0][0] if rm_rate else 0
rm_base_rate = bom_rates[0].get("base_rate") if bom_rates else 0
# Reset item valuation rate
reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10)
reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_base_rate + 10)
# update cost of all BOMs based on latest valuation rate
update_cost()
update_cost_in_all_boms_in_test()
# check if new valuation rate updated in all BOMs
for d in frappe.db.sql(
"""select rate from `tabBOM Item`
"""select base_rate from `tabBOM Item`
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
as_dict=1,
):
self.assertEqual(d.rate, rm_rate + 10)
self.assertEqual(d.base_rate, rm_base_rate + 10)
def test_bom_cost(self):
bom = frappe.copy_doc(test_records[2])

View File

@ -32,6 +32,7 @@
"is_active": 1,
"is_default": 1,
"item": "_Test Item Home Desktop Manufactured",
"company": "_Test Company",
"quantity": 1.0
},
{

View File

@ -169,13 +169,15 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-08 16:21:29.386212",
"modified": "2022-05-27 13:42:23.305455",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Explosion Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,54 @@
{
"actions": [],
"autoname": "hash",
"creation": "2022-05-31 17:34:39.825537",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"level",
"batch_no",
"boms_updated",
"status"
],
"fields": [
{
"fieldname": "level",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Level"
},
{
"fieldname": "batch_no",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Batch No."
},
{
"fieldname": "boms_updated",
"fieldtype": "Long Text",
"hidden": 1,
"in_list_view": 1,
"label": "BOMs Updated"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nCompleted",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-06-06 14:50:35.161062",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Update Batch",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class BOMUpdateBatch(Document):
pass

View File

@ -13,6 +13,10 @@
"update_type",
"status",
"error_log",
"progress_section",
"current_level",
"processed_boms",
"bom_batches",
"amended_from"
],
"fields": [
@ -63,13 +67,36 @@
"fieldtype": "Link",
"label": "Error Log",
"options": "Error Log"
},
{
"collapsible": 1,
"depends_on": "eval: doc.update_type == \"Update Cost\"",
"fieldname": "progress_section",
"fieldtype": "Section Break",
"label": "Progress"
},
{
"fieldname": "processed_boms",
"fieldtype": "Long Text",
"hidden": 1,
"label": "Processed BOMs"
},
{
"fieldname": "bom_batches",
"fieldtype": "Table",
"options": "BOM Update Batch"
},
{
"fieldname": "current_level",
"fieldtype": "Int",
"label": "Current Level"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-03-31 12:51:44.885102",
"modified": "2022-06-06 15:15:23.883251",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Update Log",

View File

@ -1,13 +1,20 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from typing import Dict, List, Literal, Optional
import json
from typing import Any, Dict, List, Optional, Tuple, Union
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, flt
from frappe.utils import cint, cstr
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
get_leaf_boms,
get_next_higher_level_boms,
handle_exception,
replace_bom,
set_values_in_log,
)
class BOMMissingError(frappe.ValidationError):
@ -20,6 +27,8 @@ class BOMUpdateLog(Document):
self.validate_boms_are_specified()
self.validate_same_bom()
self.validate_bom_items()
else:
self.validate_bom_cost_update_in_progress()
self.status = "Queued"
@ -42,123 +51,184 @@ class BOMUpdateLog(Document):
if current_bom_item != new_bom_item:
frappe.throw(_("The selected BOMs are not for the same item"))
def on_submit(self):
if frappe.flags.in_test:
return
def validate_bom_cost_update_in_progress(self):
"If another Cost Updation Log is still in progress, dont make new ones."
wip_log = frappe.get_all(
"BOM Update Log",
{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
limit_page_length=1,
)
if wip_log:
log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name)
frappe.throw(
_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),
title=_("Note"),
)
def on_submit(self):
if self.update_type == "Replace BOM":
boms = {"current_bom": self.current_bom, "new_bom": self.new_bom}
frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_replace_bom_job",
doc=self,
boms=boms,
timeout=40000,
now=frappe.flags.in_test,
)
else:
frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
doc=self,
update_type="Update Cost",
timeout=40000,
)
process_boms_cost_level_wise(self)
def replace_bom(boms: Dict) -> None:
"""Replace current BOM with new BOM in parent BOMs."""
current_bom = boms.get("current_bom")
new_bom = boms.get("new_bom")
unit_cost = get_new_bom_unit_cost(new_bom)
update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
frappe.cache().delete_key("bom_children")
parent_boms = get_parent_boms(new_bom)
for bom in parent_boms:
bom_obj = frappe.get_doc("BOM", bom)
# this is only used for versioning and we do not want
# to make separate db calls by using load_doc_before_save
# which proves to be expensive while doing bulk replace
bom_obj._doc_before_save = bom_obj
bom_obj.update_exploded_items()
bom_obj.calculate_cost()
bom_obj.update_parent_cost()
bom_obj.db_update()
if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version:
bom_obj.save_version()
def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
bom_item = frappe.qb.DocType("BOM Item")
(
frappe.qb.update(bom_item)
.set(bom_item.bom_no, new_bom)
.set(bom_item.rate, unit_cost)
.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
.where(
(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
)
).run()
def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
bom_list = bom_list or []
bom_item = frappe.qb.DocType("BOM Item")
parents = (
frappe.qb.from_(bom_item)
.select(bom_item.parent)
.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
.run(as_dict=True)
)
for d in parents:
if new_bom == d.parent:
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
bom_list.append(d.parent)
get_parent_boms(d.parent, bom_list)
return list(set(bom_list))
def get_new_bom_unit_cost(new_bom: str) -> float:
bom = frappe.qb.DocType("BOM")
new_bom_unitcost = (
frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run()
)
return flt(new_bom_unitcost[0][0])
def run_bom_job(
def run_replace_bom_job(
doc: "BOMUpdateLog",
boms: Optional[Dict[str, str]] = None,
update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
) -> None:
try:
doc.db_set("status", "In Progress")
if not frappe.flags.in_test:
frappe.db.commit()
frappe.db.auto_commit_on_many_writes = 1
boms = frappe._dict(boms or {})
if update_type == "Replace BOM":
replace_bom(boms)
else:
update_cost()
replace_bom(boms, doc.name)
doc.db_set("status", "Completed")
except Exception:
frappe.db.rollback()
error_log = doc.log_error("BOM Update Tool Error")
doc.db_set("status", "Failed")
doc.db_set("error_log", error_log.name)
handle_exception(doc)
finally:
frappe.db.auto_commit_on_many_writes = 0
frappe.db.commit() # nosemgrep
if not frappe.flags.in_test:
frappe.db.commit() # nosemgrep
def process_boms_cost_level_wise(
update_doc: "BOMUpdateLog", parent_boms: List[str] = None
) -> Union[None, Tuple]:
"Queue jobs at the start of new BOM Level in 'Update Cost' Jobs."
current_boms = {}
values = {}
if update_doc.status == "Queued":
# First level yet to process. On Submit.
current_level = 0
current_boms = get_leaf_boms()
values = {
"processed_boms": json.dumps({}),
"status": "In Progress",
"current_level": current_level,
}
else:
# Resume next level. via Cron Job.
if not parent_boms:
return
current_level = cint(update_doc.current_level) + 1
# Process the next level BOMs. Stage parents as current BOMs.
current_boms = parent_boms.copy()
values = {"current_level": current_level}
set_values_in_log(update_doc.name, values, commit=True)
queue_bom_cost_jobs(current_boms, update_doc, current_level)
def queue_bom_cost_jobs(
current_boms_list: List[str], update_doc: "BOMUpdateLog", current_level: int
) -> None:
"Queue batches of 20k BOMs of the same level to process parallelly"
batch_no = 0
while current_boms_list:
batch_no += 1
batch_size = 20_000
boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs
# update list to exclude 20K (queued) BOMs
current_boms_list = current_boms_list[batch_size:] if len(current_boms_list) > batch_size else []
batch_row = update_doc.append(
"bom_batches", {"level": current_level, "batch_no": batch_no, "status": "Pending"}
)
batch_row.db_insert()
frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level",
doc=update_doc,
bom_list=boms_to_process,
batch_name=batch_row.name,
queue="long",
now=frappe.flags.in_test,
)
def resume_bom_cost_update_jobs():
"""
1. Checks for In Progress BOM Update Log.
2. Checks if this job has completed the _current level_.
3. If current level is complete, get parent BOMs and start next level.
4. If no parents, mark as Complete.
5. If current level is WIP, skip the Log.
Called every 5 minutes via Cron job.
"""
in_progress_logs = frappe.db.get_all(
"BOM Update Log",
{"update_type": "Update Cost", "status": "In Progress"},
["name", "processed_boms", "current_level"],
)
if not in_progress_logs:
return
for log in in_progress_logs:
# check if all log batches of current level are processed
bom_batches = frappe.db.get_all(
"BOM Update Batch",
{"parent": log.name, "level": log.current_level},
["name", "boms_updated", "status"],
)
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
if not bom_batches or incomplete_level:
continue
# Prep parent BOMs & updated processed BOMs for next level
current_boms, processed_boms = get_processed_current_boms(log, bom_batches)
parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms)
# Unset processed BOMs if log is complete, it is used for next level BOMs
set_values_in_log(
log.name,
values={
"processed_boms": json.dumps([] if not parent_boms else processed_boms),
"status": "Completed" if not parent_boms else "In Progress",
},
commit=True,
)
if parent_boms: # there is a next level to process
process_boms_cost_level_wise(
update_doc=frappe.get_doc("BOM Update Log", log.name), parent_boms=parent_boms
)
def get_processed_current_boms(
log: Dict[str, Any], bom_batches: Dict[str, Any]
) -> Tuple[List[str], Dict[str, Any]]:
"""
Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field
and into current boms list.
"""
processed_boms = json.loads(log.processed_boms) if log.processed_boms else {}
current_boms = []
for row in bom_batches:
boms_updated = json.loads(row.boms_updated)
current_boms.extend(boms_updated)
boms_updated_dict = {bom: True for bom in boms_updated}
processed_boms.update(boms_updated_dict)
return current_boms, processed_boms

View File

@ -0,0 +1,225 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
import json
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
if TYPE_CHECKING:
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog
import frappe
from frappe import _
def replace_bom(boms: Dict, log_name: str) -> None:
"Replace current BOM with new BOM in parent BOMs."
current_bom = boms.get("current_bom")
new_bom = boms.get("new_bom")
unit_cost = get_bom_unit_cost(new_bom)
update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
frappe.cache().delete_key("bom_children")
parent_boms = get_ancestor_boms(new_bom)
for bom in parent_boms:
bom_obj = frappe.get_doc("BOM", bom)
# this is only used for versioning and we do not want
# to make separate db calls by using load_doc_before_save
# which proves to be expensive while doing bulk replace
bom_obj._doc_before_save = copy.deepcopy(bom_obj)
bom_obj.update_exploded_items()
bom_obj.calculate_cost()
bom_obj.update_parent_cost()
bom_obj.db_update()
bom_obj.flags.updater_reference = {
"doctype": "BOM Update Log",
"docname": log_name,
"label": _("via BOM Update Tool"),
}
bom_obj.save_version()
def update_cost_in_level(
doc: "BOMUpdateLog", bom_list: List[str], batch_name: Union[int, str]
) -> None:
"Updates Cost for BOMs within a given level. Runs via background jobs."
try:
status = frappe.db.get_value("BOM Update Log", doc.name, "status")
if status == "Failed":
return
update_cost_in_boms(bom_list=bom_list) # main updation logic
bom_batch = frappe.qb.DocType("BOM Update Batch")
(
frappe.qb.update(bom_batch)
.set(bom_batch.boms_updated, json.dumps(bom_list))
.set(bom_batch.status, "Completed")
.where(bom_batch.name == batch_name)
).run()
except Exception:
handle_exception(doc)
finally:
if not frappe.flags.in_test:
frappe.db.commit() # nosemgrep
def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
"Recursively get all ancestors of BOM."
bom_list = bom_list or []
bom_item = frappe.qb.DocType("BOM Item")
parents = (
frappe.qb.from_(bom_item)
.select(bom_item.parent)
.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
.run(as_dict=True)
)
for d in parents:
if new_bom == d.parent:
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
bom_list.append(d.parent)
get_ancestor_boms(d.parent, bom_list)
return list(set(bom_list))
def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
bom_item = frappe.qb.DocType("BOM Item")
(
frappe.qb.update(bom_item)
.set(bom_item.bom_no, new_bom)
.set(bom_item.rate, unit_cost)
.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
.where(
(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
)
).run()
def get_bom_unit_cost(bom_name: str) -> float:
bom = frappe.qb.DocType("BOM")
new_bom_unitcost = (
frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == bom_name).run()
)
return frappe.utils.flt(new_bom_unitcost[0][0])
def update_cost_in_boms(bom_list: List[str]) -> None:
"Updates cost in given BOMs. Returns current and total updated BOMs."
for index, bom in enumerate(bom_list):
bom_doc = frappe.get_doc("BOM", bom, for_update=True)
bom_doc.calculate_cost(save_updates=True, update_hour_rate=True)
bom_doc.db_update()
if (index % 50 == 0) and not frappe.flags.in_test:
frappe.db.commit() # nosemgrep
def get_next_higher_level_boms(
child_boms: List[str], processed_boms: Dict[str, bool]
) -> List[str]:
"Generate immediate higher level dependants with no unresolved dependencies (children)."
def _all_children_are_processed(parent_bom):
child_boms = dependency_map.get(parent_bom)
return all(processed_boms.get(bom) for bom in child_boms)
dependants_map, dependency_map = _generate_dependence_map()
dependants = []
for bom in child_boms:
# generate list of immediate dependants
parents = dependants_map.get(bom) or []
dependants.extend(parents)
dependants = set(dependants) # remove duplicates
resolved_dependants = set()
# consider only if children are all resolved
for parent_bom in dependants:
if _all_children_are_processed(parent_bom):
resolved_dependants.add(parent_bom)
return list(resolved_dependants)
def get_leaf_boms() -> List[str]:
"Get BOMs that have no dependencies."
return frappe.db.sql_list(
"""select name from `tabBOM` bom
where docstatus=1 and is_active=1
and not exists(select bom_no from `tabBOM Item`
where parent=bom.name and ifnull(bom_no, '')!='')"""
)
def _generate_dependence_map() -> defaultdict:
"""
Generate maps such as: { BOM-1: [Dependant-BOM-1, Dependant-BOM-2, ..] }.
Here BOM-1 is the leaf/lower level node/dependency.
The list contains one level higher nodes/dependants that depend on BOM-1.
Generate and return the reverse as well.
"""
bom = frappe.qb.DocType("BOM")
bom_item = frappe.qb.DocType("BOM Item")
bom_items = (
frappe.qb.from_(bom_item)
.join(bom)
.on(bom_item.parent == bom.name)
.select(bom_item.bom_no, bom_item.parent)
.where(
(bom_item.bom_no.isnotnull())
& (bom_item.bom_no != "")
& (bom.docstatus == 1)
& (bom.is_active == 1)
& (bom_item.parenttype == "BOM")
)
).run(as_dict=True)
child_parent_map = defaultdict(list)
parent_child_map = defaultdict(list)
for row in bom_items:
child_parent_map[row.bom_no].append(row.parent)
parent_child_map[row.parent].append(row.bom_no)
return child_parent_map, parent_child_map
def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = False) -> None:
"Update BOM Update Log record."
if not values:
return
bom_update_log = frappe.qb.DocType("BOM Update Log")
query = frappe.qb.update(bom_update_log).where(bom_update_log.name == log_name)
for key, value in values.items():
query = query.set(key, value)
query.run()
if commit and not frappe.flags.in_test:
frappe.db.commit() # nosemgrep
def handle_exception(doc: "BOMUpdateLog") -> None:
"Rolls back and fails BOM Update Log."
frappe.db.rollback()
error_log = doc.log_error("BOM Update Tool Error")
set_values_in_log(doc.name, {"status": "Failed", "error_log": error_log.name})

View File

@ -6,9 +6,12 @@ from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import (
BOMMissingError,
run_bom_job,
resume_bom_cost_update_jobs,
)
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import (
enqueue_replace_bom,
enqueue_update_cost,
)
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
test_records = frappe.get_test_records("BOM")
@ -31,17 +34,12 @@ class TestBOMUpdateLog(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
if self._testMethodName == "test_bom_update_log_completion":
# clear logs and delete BOM created via setUp
frappe.db.delete("BOM Update Log")
self.new_bom_doc.cancel()
self.new_bom_doc.delete()
# explicitly commit and restore to original state
frappe.db.commit() # nosemgrep
def test_bom_update_log_validate(self):
"Test if BOM presence is validated."
"""
1) Test if BOM presence is validated.
2) Test if same BOMs are validated.
3) Test of non-existent BOM is validated.
"""
with self.assertRaises(BOMMissingError):
enqueue_replace_bom(boms={})
@ -52,45 +50,22 @@ class TestBOMUpdateLog(FrappeTestCase):
with self.assertRaises(frappe.ValidationError):
enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM"))
def test_bom_update_log_queueing(self):
"Test if BOM Update Log is created and queued."
log = enqueue_replace_bom(
boms=self.boms,
)
self.assertEqual(log.docstatus, 1)
self.assertEqual(log.status, "Queued")
def test_bom_update_log_completion(self):
"Test if BOM Update Log handles job completion correctly."
log = enqueue_replace_bom(
boms=self.boms,
)
# Explicitly commits log, new bom (setUp) and replacement impact.
# Is run via background jobs IRL
run_bom_job(
doc=log,
boms=self.boms,
update_type="Replace BOM",
)
log = enqueue_replace_bom(boms=self.boms)
log.reload()
self.assertEqual(log.status, "Completed")
# teardown (undo replace impact) due to commit
boms = frappe._dict(
current_bom=self.boms.new_bom,
new_bom=self.boms.current_bom,
)
log2 = enqueue_replace_bom(
boms=self.boms,
)
run_bom_job( # Explicitly commits
doc=log2,
boms=boms,
update_type="Replace BOM",
)
self.assertEqual(log2.status, "Completed")
def update_cost_in_all_boms_in_test():
"""
Utility to run 'Update Cost' job in tests without Cron job until fully complete.
"""
log = enqueue_update_cost() # create BOM Update Log
while log.status != "Completed":
resume_bom_cost_update_jobs() # run cron job until complete
log.reload()
return log

View File

@ -10,8 +10,6 @@ if TYPE_CHECKING:
import frappe
from frappe.model.document import Document
from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
class BOMUpdateTool(Document):
pass
@ -40,14 +38,13 @@ def enqueue_update_cost() -> "BOMUpdateLog":
def auto_update_latest_price_in_all_boms() -> None:
"""Called via hooks.py."""
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
update_cost()
def update_cost() -> None:
"""Updates Cost for all BOMs from bottom to top."""
bom_list = get_boms_in_bottom_up_order()
for bom in bom_list:
frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
wip_log = frappe.get_all(
"BOM Update Log",
{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
limit_page_length=1,
)
if not wip_log:
create_bom_update_log(update_type="Update Cost")
def create_bom_update_log(

View File

@ -1,11 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
update_cost_in_all_boms_in_test,
)
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item
@ -15,6 +17,9 @@ test_records = frappe.get_test_records("BOM")
class TestBOMUpdateTool(FrappeTestCase):
"Test major functions run via BOM Update Tool."
def tearDown(self):
frappe.db.rollback()
def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
@ -23,15 +28,10 @@ class TestBOMUpdateTool(FrappeTestCase):
bom_doc.insert()
boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
replace_bom(boms)
enqueue_replace_bom(boms=boms)
self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom))
self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name))
# reverse, as it affects other testcases
boms.current_bom = bom_doc.name
boms.new_bom = current_bom
replace_bom(boms)
self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
def test_bom_cost(self):
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
@ -52,13 +52,13 @@ class TestBOMUpdateTool(FrappeTestCase):
self.assertEqual(doc.total_cost, 200)
frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200)
update_cost()
update_cost_in_all_boms_in_test()
doc.load_from_db()
self.assertEqual(doc.total_cost, 300)
frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100)
update_cost()
update_cost_in_all_boms_in_test()
doc.load_from_db()
self.assertEqual(doc.total_cost, 200)

View File

@ -626,14 +626,15 @@ class JobCard(Document):
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
if self.for_quantity <= self.transferred_qty:
self.status = "Material Transferred"
if self.docstatus < 2:
if self.for_quantity <= self.transferred_qty:
self.status = "Material Transferred"
if self.time_logs:
self.status = "Work In Progress"
if self.time_logs:
self.status = "Work In Progress"
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
self.status = "Completed"
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
self.status = "Completed"
if update_status:
self.db_set("status", self.status)

View File

@ -1,16 +1,17 @@
frappe.listview_settings['Job Card'] = {
has_indicator_for_draft: true,
get_indicator: function(doc) {
if (doc.status === "Work In Progress") {
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
} else if (doc.status === "Completed") {
return [__("Completed"), "green", "status,=,Completed"];
} else if (doc.docstatus == 2) {
return [__("Cancelled"), "red", "status,=,Cancelled"];
} else if (doc.status === "Material Transferred") {
return [__('Material Transferred'), "blue", "status,=,Material Transferred"];
} else {
return [__("Open"), "red", "status,=,Open"];
}
const status_colors = {
"Work In Progress": "orange",
"Completed": "green",
"Cancelled": "red",
"Material Transferred": "blue",
"Open": "red",
};
const status = doc.status || "Open";
const color = status_colors[status] || "blue";
return [__(status), color, `status,=,${status}`];
}
};

View File

@ -344,6 +344,30 @@ class TestJobCard(FrappeTestCase):
cost_after_cancel = self.work_order.total_operating_cost
self.assertEqual(cost_after_cancel, original_cost)
def test_job_card_statuses(self):
def assertStatus(status):
jc.set_status()
self.assertEqual(jc.status, status)
jc = frappe.new_doc("Job Card")
jc.for_quantity = 2
jc.transferred_qty = 1
jc.total_completed_qty = 0
assertStatus("Open")
jc.transferred_qty = jc.for_quantity
assertStatus("Material Transferred")
jc.append("time_logs", {})
assertStatus("Work In Progress")
jc.docstatus = 1
jc.total_completed_qty = jc.for_quantity
assertStatus("Completed")
jc.docstatus = 2
assertStatus("Cancelled")
def create_bom_with_multiple_operations():
"Create a BOM with multiple operations and Material Transfer against Job Card"

View File

@ -849,7 +849,7 @@ def get_subitems(
FROM
`tabBOM Item` bom_item
JOIN `tabBOM` bom ON bom.name = bom_item.parent
JOIN tabItem item ON bom_item.item_code = item.name
JOIN `tabItem` item ON bom_item.item_code = item.name
LEFT JOIN `tabItem Default` item_default
ON item.name = item_default.parent and item_default.company = %(company)s
LEFT JOIN `tabUOM Conversion Detail` item_uom
@ -979,7 +979,7 @@ def get_sales_orders(self):
select distinct so.name, so.transaction_date, so.customer, so.base_grand_total
from `tabSales Order` so, `tabSales Order Item` so_item
where so_item.parent = so.name
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
and so.docstatus = 1 and so.status not in ('Stopped', 'Closed')
and so.company = %(company)s
and so_item.qty > so_item.work_order_qty {so_filter} {item_filter}
and (exists (select name from `tabBOM` bom where {bom_item}

View File

@ -798,7 +798,6 @@ def make_bom(**args):
for item in args.raw_materials:
item_doc = frappe.get_doc("Item", item)
bom.append(
"items",
{

View File

@ -417,7 +417,7 @@ class TestWorkOrder(FrappeTestCase):
"doctype": "Item Price",
"item_code": "_Test FG Non Stock Item",
"price_list_rate": 1000,
"price_list": "Standard Buying",
"price_list": "_Test Price List India",
}
).insert(ignore_permissions=True)
@ -426,8 +426,17 @@ class TestWorkOrder(FrappeTestCase):
item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100
)
if not frappe.db.get_value("BOM", {"item": fg_item}):
make_bom(item=fg_item, rate=1000, raw_materials=["_Test FG Item", "_Test FG Non Stock Item"])
if not frappe.db.get_value("BOM", {"item": fg_item, "docstatus": 1}):
bom = make_bom(
item=fg_item,
rate=1000,
raw_materials=["_Test FG Item", "_Test FG Non Stock Item"],
do_not_save=True,
)
bom.rm_cost_as_per = "Price List" # non stock item won't have valuation rate
bom.buying_price_list = "_Test Price List India"
bom.currency = "INR"
bom.save()
wo = make_wo_order_test_record(production_item=fg_item)

View File

@ -939,7 +939,7 @@ class WorkOrder(Document):
from `tabStock Entry` entry, `tabStock Entry Detail` detail
where
entry.work_order = %(name)s
and entry.purpose = "Material Transfer for Manufacture"
and entry.purpose = 'Material Transfer for Manufacture'
and entry.docstatus = 1
and detail.parent = entry.name
and (detail.item_code = %(item)s or detail.original_item = %(item)s)""",

View File

@ -102,7 +102,7 @@ def get_work_orders(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
"""select name from `tabWork Order`
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1
order by name limit {1}, {2}""".format(
order by name limit {2} offset {1}""".format(
cond, start, page_len
),
{"name": "%%%s%%" % txt},

View File

@ -1,6 +1,6 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"docstatus": 0,
"doctype": "Workspace",
@ -402,7 +402,7 @@
"type": "Link"
}
],
"modified": "2022-05-31 22:08:19.408223",
"modified": "2022-06-15 15:18:57.062935",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@ -415,39 +415,35 @@
"sequence_id": 17.0,
"shortcuts": [
{
"color": "Green",
"format": "{} Active",
"label": "Item",
"link_to": "Item",
"restrict_to_domain": "Manufacturing",
"stats_filter": "{\n \"disabled\": 0\n}",
"type": "DocType"
},
{
"color": "Green",
"format": "{} Active",
"color": "Grey",
"doc_view": "List",
"label": "BOM",
"link_to": "BOM",
"restrict_to_domain": "Manufacturing",
"stats_filter": "{\n \"is_active\": 1\n}",
"stats_filter": "{\"is_active\":[\"=\",1]}",
"type": "DocType"
},
{
"color": "Yellow",
"format": "{} Open",
"label": "Work Order",
"link_to": "Work Order",
"restrict_to_domain": "Manufacturing",
"stats_filter": "{ \n \"status\": [\"in\", \n [\"Draft\", \"Not Started\", \"In Process\"]\n ]\n}",
"type": "DocType"
},
{
"color": "Yellow",
"format": "{} Open",
"color": "Grey",
"doc_view": "List",
"label": "Production Plan",
"link_to": "Production Plan",
"restrict_to_domain": "Manufacturing",
"stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}",
"stats_filter": "{\"status\":[\"not in\",[\"Closed\",\"Cancelled\",\"Completed\"]]}",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Work Order",
"link_to": "Work Order",
"stats_filter": "{\"status\":[\"not in\",[\"Closed\",\"Cancelled\",\"Completed\"]]}",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Job Card",
"link_to": "Job Card",
"stats_filter": "{\"status\":[\"not in\",[\"Cancelled\",\"Completed\",null]]}",
"type": "DocType"
},
{
@ -455,12 +451,6 @@
"link_to": "Exponential Smoothing Forecasting",
"type": "Report"
},
{
"label": "Work Order Summary",
"link_to": "Work Order Summary",
"restrict_to_domain": "Manufacturing",
"type": "Report"
},
{
"label": "BOM Stock Report",
"link_to": "BOM Stock Report",
@ -470,12 +460,6 @@
"label": "Production Planning Report",
"link_to": "Production Planning Report",
"type": "Report"
},
{
"label": "Dashboard",
"link_to": "Manufacturing",
"restrict_to_domain": "Manufacturing",
"type": "Dashboard"
}
],
"title": "Manufacturing"

View File

@ -339,7 +339,7 @@ erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v14_0.delete_healthcare_doctypes
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
erpnext.patches.v14_0.delete_agriculture_doctypes
erpnext.patches.v14_0.delete_agriculture_doctypes # 15-06-2022
erpnext.patches.v14_0.delete_education_doctypes
erpnext.patches.v14_0.delete_datev_doctypes
erpnext.patches.v14_0.rearrange_company_fields
@ -374,3 +374,4 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
execute:frappe.delete_doc("DocType", "Naming Series")
erpnext.patches.v13_0.set_payroll_entry_status
erpnext.patches.v13_0.job_card_status_on_hold
erpnext.patches.v14_0.migrate_gl_to_payment_ledger

View File

@ -1,131 +0,0 @@
import frappe
from frappe.model.utils.rename_field import rename_field
from frappe.modules import get_doctype_module, scrub
field_rename_map = {
"Healthcare Settings": [
["patient_master_name", "patient_name_by"],
["max_visit", "max_visits"],
["reg_sms", "send_registration_msg"],
["reg_msg", "registration_msg"],
["app_con", "send_appointment_confirmation"],
["app_con_msg", "appointment_confirmation_msg"],
["no_con", "avoid_confirmation"],
["app_rem", "send_appointment_reminder"],
["app_rem_msg", "appointment_reminder_msg"],
["rem_before", "remind_before"],
["manage_customer", "link_customer_to_patient"],
["create_test_on_si_submit", "create_lab_test_on_si_submit"],
["require_sample_collection", "create_sample_collection_for_lab_test"],
["require_test_result_approval", "lab_test_approval_required"],
["manage_appointment_invoice_automatically", "automate_appointment_invoicing"],
],
"Drug Prescription": [["use_interval", "usage_interval"], ["in_every", "interval_uom"]],
"Lab Test Template": [
["sample_quantity", "sample_qty"],
["sample_collection_details", "sample_details"],
],
"Sample Collection": [
["sample_quantity", "sample_qty"],
["sample_collection_details", "sample_details"],
],
"Fee Validity": [["max_visit", "max_visits"]],
}
def execute():
for dn in field_rename_map:
if frappe.db.exists("DocType", dn):
if dn == "Healthcare Settings":
frappe.reload_doctype("Healthcare Settings")
else:
frappe.reload_doc(get_doctype_module(dn), "doctype", scrub(dn))
for dt, field_list in field_rename_map.items():
if frappe.db.exists("DocType", dt):
for field in field_list:
if dt == "Healthcare Settings":
rename_field(dt, field[0], field[1])
elif frappe.db.has_column(dt, field[0]):
rename_field(dt, field[0], field[1])
# first name mandatory in Patient
if frappe.db.exists("DocType", "Patient"):
patients = frappe.db.sql("select name, patient_name from `tabPatient`", as_dict=1)
frappe.reload_doc("healthcare", "doctype", "patient")
for entry in patients:
name = entry.patient_name.split(" ")
frappe.db.set_value("Patient", entry.name, "first_name", name[0])
# mark Healthcare Practitioner status as Disabled
if frappe.db.exists("DocType", "Healthcare Practitioner"):
practitioners = frappe.db.sql(
"select name from `tabHealthcare Practitioner` where 'active'= 0", as_dict=1
)
practitioners_lst = [p.name for p in practitioners]
frappe.reload_doc("healthcare", "doctype", "healthcare_practitioner")
if practitioners_lst:
frappe.db.sql(
"update `tabHealthcare Practitioner` set status = 'Disabled' where name IN %(practitioners)s"
"",
{"practitioners": practitioners_lst},
)
# set Clinical Procedure status
if frappe.db.exists("DocType", "Clinical Procedure"):
frappe.reload_doc("healthcare", "doctype", "clinical_procedure")
frappe.db.sql(
"""
UPDATE
`tabClinical Procedure`
SET
docstatus = (CASE WHEN status = 'Cancelled' THEN 2
WHEN status = 'Draft' THEN 0
ELSE 1
END)
"""
)
# set complaints and diagnosis in table multiselect in Patient Encounter
if frappe.db.exists("DocType", "Patient Encounter"):
field_list = [["visit_department", "medical_department"], ["type", "appointment_type"]]
encounter_details = frappe.db.sql(
"""select symptoms, diagnosis, name from `tabPatient Encounter`""", as_dict=True
)
frappe.reload_doc("healthcare", "doctype", "patient_encounter")
frappe.reload_doc("healthcare", "doctype", "patient_encounter_symptom")
frappe.reload_doc("healthcare", "doctype", "patient_encounter_diagnosis")
for field in field_list:
if frappe.db.has_column(dt, field[0]):
rename_field(dt, field[0], field[1])
for entry in encounter_details:
doc = frappe.get_doc("Patient Encounter", entry.name)
symptoms = entry.symptoms.split("\n") if entry.symptoms else []
for symptom in symptoms:
if not frappe.db.exists("Complaint", symptom):
frappe.get_doc({"doctype": "Complaint", "complaints": symptom}).insert()
row = doc.append("symptoms", {"complaint": symptom})
row.db_update()
diagnosis = entry.diagnosis.split("\n") if entry.diagnosis else []
for d in diagnosis:
if not frappe.db.exists("Diagnosis", d):
frappe.get_doc({"doctype": "Diagnosis", "diagnosis": d}).insert()
row = doc.append("diagnosis", {"diagnosis": d})
row.db_update()
doc.db_update()
if frappe.db.exists("DocType", "Fee Validity"):
# update fee validity status
frappe.db.sql(
"""
UPDATE
`tabFee Validity`
SET
status = (CASE WHEN visited >= max_visits THEN 'Completed'
ELSE 'Pending'
END)
"""
)

View File

@ -1,94 +0,0 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if frappe.db.exists("DocType", "Lab Test") and frappe.db.exists("DocType", "Lab Test Template"):
# rename child doctypes
doctypes = {
"Lab Test Groups": "Lab Test Group Template",
"Normal Test Items": "Normal Test Result",
"Sensitivity Test Items": "Sensitivity Test Result",
"Special Test Items": "Descriptive Test Result",
"Special Test Template": "Descriptive Test Template",
}
frappe.reload_doc("healthcare", "doctype", "lab_test")
frappe.reload_doc("healthcare", "doctype", "lab_test_template")
for old_dt, new_dt in doctypes.items():
frappe.flags.link_fields = {}
should_rename = frappe.db.table_exists(old_dt) and not frappe.db.table_exists(new_dt)
if should_rename:
frappe.reload_doc("healthcare", "doctype", frappe.scrub(old_dt))
frappe.rename_doc("DocType", old_dt, new_dt, force=True)
frappe.reload_doc("healthcare", "doctype", frappe.scrub(new_dt))
frappe.delete_doc_if_exists("DocType", old_dt)
parent_fields = {
"Lab Test Group Template": "lab_test_groups",
"Descriptive Test Template": "descriptive_test_templates",
"Normal Test Result": "normal_test_items",
"Sensitivity Test Result": "sensitivity_test_items",
"Descriptive Test Result": "descriptive_test_items",
}
for doctype, parentfield in parent_fields.items():
frappe.db.sql(
"""
UPDATE `tab{0}`
SET parentfield = %(parentfield)s
""".format(
doctype
),
{"parentfield": parentfield},
)
# copy renamed child table fields (fields were already renamed in old doctype json, hence sql)
rename_fields = {
"lab_test_name": "test_name",
"lab_test_event": "test_event",
"lab_test_uom": "test_uom",
"lab_test_comment": "test_comment",
}
for new, old in rename_fields.items():
if frappe.db.has_column("Normal Test Result", old):
frappe.db.sql("""UPDATE `tabNormal Test Result` SET {} = {}""".format(new, old))
if frappe.db.has_column("Normal Test Template", "test_event"):
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""")
if frappe.db.has_column("Normal Test Template", "test_uom"):
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""")
if frappe.db.has_column("Descriptive Test Result", "test_particulars"):
frappe.db.sql(
"""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars"""
)
rename_fields = {
"lab_test_template": "test_template",
"lab_test_description": "test_description",
"lab_test_rate": "test_rate",
}
for new, old in rename_fields.items():
if frappe.db.has_column("Lab Test Group Template", old):
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET {} = {}""".format(new, old))
# rename field
frappe.reload_doc("healthcare", "doctype", "lab_test")
if frappe.db.has_column("Lab Test", "special_toggle"):
rename_field("Lab Test", "special_toggle", "descriptive_toggle")
if frappe.db.exists("DocType", "Lab Test Group Template"):
# fix select field option
frappe.reload_doc("healthcare", "doctype", "lab_test_group_template")
frappe.db.sql(
"""
UPDATE `tabLab Test Group Template`
SET template_or_new_line = 'Add New Line'
WHERE template_or_new_line = 'Add new line'
"""
)

View File

@ -1,9 +0,0 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from erpnext.setup.install import create_print_uom_after_qty_custom_field
def execute():
create_print_uom_after_qty_custom_field()

Some files were not shown because too many files have changed in this diff Show More