Merge branch 'develop' into fix-reserve-qty

This commit is contained in:
Devin Slauenwhite 2022-06-17 14:39:31 -04:00 committed by GitHub
commit 7855a59843
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
127 changed files with 2102 additions and 1219 deletions

View File

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

View File

@ -9,6 +9,8 @@ pull_request_rules:
- author!=nabinhait - author!=nabinhait
- author!=ankush - author!=ankush
- author!=deepeshgarg007 - author!=deepeshgarg007
- author!=mergify[bot]
- or: - or:
- base=version-13 - base=version-13
- base=version-12 - 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. @{{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 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 - name: backport to develop
conditions: conditions:
- label="backport develop" - label="backport develop"

View File

@ -10,4 +10,42 @@ Entries are:
- Sales Invoice (Itemised) - Sales Invoice (Itemised)
- Purchase 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( return frappe.db.sql(
"""select name from tabAccount """select name from tabAccount
where is_group = 1 and docstatus != 2 and company = %s 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"), % ("%s", searchfield, "%s", "%s", "%s"),
(filters["company"], "%%%s%%" % txt, start, page_len), (filters["company"], "%%%s%%" % txt, page_len, start),
as_list=1, as_list=1,
) )

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import json
from functools import reduce from functools import reduce
import frappe 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 from frappe.utils import cint, comma_or, flt, getdate, nowdate
import erpnext import erpnext
@ -785,7 +785,7 @@ class PaymentEntry(AccountsController):
self.set("remarks", "\n".join(remarks)) 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"): if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
self.setup_party_account_field() self.setup_party_account_field()
@ -794,7 +794,10 @@ class PaymentEntry(AccountsController):
self.add_bank_gl_entries(gl_entries) self.add_bank_gl_entries(gl_entries)
self.add_deductions_gl_entries(gl_entries) self.add_deductions_gl_entries(gl_entries)
self.add_tax_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) gl_entries = process_gl_map(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) 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": if args.get("party_type") == "Member":
return return
ple = qb.DocType("Payment Ledger Entry")
common_filter = []
# confirm that Supplier is not blocked # confirm that Supplier is not blocked
if args.get("party_type") == "Supplier": if args.get("party_type") == "Supplier":
supplier_status = get_supplier_block_status(args["party"]) 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( condition = " and voucher_type={0} and voucher_no={1}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]) 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 # Add cost center condition
if args.get("cost_center"): if args.get("cost_center"):
condition += " and cost_center='%s'" % 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 = { date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"], "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( condition += " and {0} between '{1}' and '{2}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1]) 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"): if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(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( outstanding_invoices = get_outstanding_invoices(
args.get("party_type"), args.get("party_type"),
args.get("party"), args.get("party"),
args.get("party_account"), args.get("party_account"),
filters=args, common_filter=common_filter,
condition=condition, 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) 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" voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = "" supplier_condition = ""
if voucher_type == "Purchase Invoice": 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: if party_account_currency == company_currency:
grand_total_field = "base_grand_total" grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total" rounded_total_field = "base_rounded_total"

View File

@ -4,6 +4,7 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import ( 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"] test_dependencies = ["Item"]
class TestPaymentEntry(unittest.TestCase): class TestPaymentEntry(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def test_payment_entry_against_order(self): def test_payment_entry_against_order(self):
so = make_sales_order() so = make_sales_order()
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")

View File

@ -6,6 +6,19 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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): class PaymentLedgerEntry(Document):
def validate_account(self): def validate_account(self):
@ -18,5 +31,119 @@ class PaymentLedgerEntry(Document):
if not valid_account: if not valid_account:
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type)) 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): def validate(self):
self.validate_account() 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( return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference` """ select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s 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}, {"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` """ select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='') (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}, {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
) )

View File

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

View File

@ -4,93 +4,453 @@
import unittest import unittest
import frappe 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.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): class TestPaymentReconciliation(FrappeTestCase):
@classmethod def setUp(self):
def setUpClass(cls): self.create_company()
make_customer() self.create_item()
make_invoice_and_payment() self.create_customer()
self.clear_old_entries()
def test_payment_reconciliation(self): def tearDown(self):
payment_reco = frappe.get_doc("Payment Reconciliation") frappe.db.rollback()
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()
self.assertEqual(len(payment_reco.get("invoices")), 1) def create_company(self):
self.assertEqual(len(payment_reco.get("payments")), 1) 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 self.company = company.name
invoice = payment_reco.get("invoices")[0].invoice_number 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( # create bank account
{ if frappe.db.exists("Account", "HDFC - _PR"):
"payments": [payment_reco.get("payments")[0].as_dict()], self.bank = "HDFC - _PR"
"invoices": [payment_reco.get("invoices")[0].as_dict()], 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) def create_customer(self):
self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice) 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(): def create_sales_invoice(
if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"): self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
frappe.get_doc( ):
{ """
"doctype": "Customer", Helper function to populate default values in sales invoice
"customer_name": "_Test Payment Reco Customer", """
"customer_type": "Individual", sinv = create_sales_invoice(
"customer_group": "_Test Customer Group", qty=qty,
"territory": "_Test Territory", rate=rate,
} company=self.company,
).insert() 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(): def clear_old_entries(self):
si = create_sales_invoice( doctype_list = [
customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True "GL Entry",
) "Payment Ledger Entry",
si.cost_center = "_Test Cost Center - _TC" "Sales Invoice",
si.save() "Purchase Invoice",
si.submit() "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( def create_payment_reconciliation(self):
{ pr = frappe.new_doc("Payment Reconciliation")
"doctype": "Payment Entry", pr.company = self.company
"payment_type": "Receive", pr.party_type = "Customer"
"party_type": "Customer", pr.party = self.customer
"party": "_Test Payment Reco Customer", pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
"company": "_Test Company", pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
"paid_from_account_currency": "INR", return pr
"paid_to_account_currency": "INR",
"source_exchange_rate": 1, def create_journal_entry(
"target_exchange_rate": 1, self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
"reference_no": "1", ):
"reference_date": getdate(), je = frappe.new_doc("Journal Entry")
"received_amount": 690, je.posting_date = posting_date or nowdate()
"paid_amount": 690, je.company = self.company
"paid_from": "Debtors - _TC", je.user_remark = "test"
"paid_to": "_Test Bank - _TC", if not cost_center:
"cost_center": "_Test Cost Center - _TC", cost_center = self.cost_center
} je.set(
) "accounts",
pe.insert() [
pe.submit() {
"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

@ -173,7 +173,7 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
where where
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
and (pf.name like %(txt)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, args,
) )

View File

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

View File

@ -1616,6 +1616,26 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
company.enable_provisional_accounting_for_non_stock_items = 0 company.enable_provisional_accounting_for_non_stock_items = 0
company.save() 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): def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql( gl_entries = frappe.db.sql(

View File

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

View File

@ -182,6 +182,7 @@
"oldfieldtype": "Currency" "oldfieldtype": "Currency"
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Stock UOM", "label": "Stock UOM",
@ -200,6 +201,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor", "fieldname": "conversion_factor",
"fieldtype": "Float", "fieldtype": "Float",
"label": "UOM Conversion Factor", "label": "UOM Conversion Factor",
@ -207,6 +209,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty", "fieldname": "stock_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Qty as per Stock UOM", "label": "Qty as per Stock UOM",
@ -843,7 +846,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-03-23 08:18:04.928287", "modified": "2022-06-17 05:33:15.335912",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "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. You shouldn't need to call this directly. Use `get_billing_cycle` instead.
""" """
plan_names = [plan.plan for plan in self.plans] plan_names = [plan.plan for plan in self.plans]
billing_info = frappe.db.sql(
"select distinct `billing_interval`, `billing_interval_count` " subscription_plan = frappe.qb.DocType("Subscription Plan")
"from `tabSubscription Plan` " billing_info = (
"where name in %s", frappe.qb.from_(subscription_plan)
(plan_names,), .select(subscription_plan.billing_interval, subscription_plan.billing_interval_count)
as_dict=1, .distinct()
) .where(subscription_plan.name.isin(plan_names))
).run(as_dict=1)
return billing_info return billing_info

View File

@ -35,7 +35,13 @@ def make_gl_entries(
validate_disabled_accounts(gl_map) validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries) gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1: 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) save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map proccess there may no be any GL Entries # Post GL Map proccess there may no be any GL Entries
elif gl_map: elif gl_map:
@ -482,6 +488,9 @@ def make_reverse_gl_entries(
if gl_entries: if gl_entries:
create_payment_ledger_entry(gl_entries, cancel=1) 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) validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj) check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) 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", "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"modified": "2022-06-07 14:29:21.352132", "modified": "2022-06-14 17:38:24.967834",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts", "name": "Accounts",

View File

@ -2,14 +2,14 @@
"action": "Create Entry", "action": "Create Entry",
"action_label": "Manage Sales Tax Templates", "action_label": "Manage Sales Tax Templates",
"creation": "2020-05-13 19:29:43.844463", "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, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2022-06-07 14:27:15.906286", "modified": "2022-06-14 17:37:56.694261",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Setup Taxes", "name": "Setup Taxes",
"owner": "Administrator", "owner": "Administrator",

View File

@ -211,7 +211,7 @@ def set_address_details(
else: else:
party_details.update(get_company_address(company)) 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: if party_details.company_address:
party_details.update( party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address) get_fetch_values(doctype, "company_address", party_details.company_address)

View File

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

View File

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

View File

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

View File

@ -100,7 +100,7 @@ def get_sales_details(filters):
sales_data = frappe.db.sql( sales_data = frappe.db.sql(
""" """
select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date, 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 from `tab{doctype}` s, `tab{doctype} Item` si
where s.name = si.parent and s.docstatus = 1 where s.name = si.parent and s.docstatus = 1
order by days_since_last_order """.format( # nosec 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): def get_mode_of_payments(filters):
mode_of_payments = {} mode_of_payments = {}
invoice_list = get_invoices(filters) 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: if invoice_list:
inv_mop = frappe.db.sql( inv_mop = frappe.db.sql(
"""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment """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 from `tabJournal Entry` a, `tabJournal Entry Account` b
where a.name = b.parent where a.name = b.parent
and a.docstatus = 1 and a.docstatus = 1
and b.reference_type = "Sales Invoice" and b.reference_type = 'Sales Invoice'
and b.reference_name in ({invoice_list_names}) and b.reference_name in ({invoice_list_names})
""".format( """.format(
invoice_list_names=invoice_list_names invoice_list_names=invoice_list_names
@ -228,7 +228,7 @@ def get_invoices(filters):
def get_mode_of_payment_details(filters): def get_mode_of_payment_details(filters):
mode_of_payment_details = {} mode_of_payment_details = {}
invoice_list = get_invoices(filters) 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: if invoice_list:
inv_mop_detail = frappe.db.sql( 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 from `tabJournal Entry` a, `tabJournal Entry Account` b
where a.name = b.parent where a.name = b.parent
and a.docstatus = 1 and a.docstatus = 1
and b.reference_type = "Sales Invoice" and b.reference_type = 'Sales Invoice'
and b.reference_name in ({invoice_list_names}) and b.reference_name in ({invoice_list_names})
group by a.owner, a.posting_date, mode_of_payment group by a.owner, a.posting_date, mode_of_payment
) t ) t

View File

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

View File

@ -3,13 +3,28 @@
from json import loads from json import loads
from typing import List, Tuple from typing import TYPE_CHECKING, List, Optional, Tuple
import frappe import frappe
import frappe.defaults import frappe.defaults
from frappe import _, qb, throw from frappe import _, qb, throw
from frappe.model.meta import get_field_precision 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 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 import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on 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): class FiscalYearError(frappe.ValidationError):
pass pass
@ -28,6 +46,9 @@ class PaymentEntryUnlinkError(frappe.ValidationError):
pass pass
GL_REPOSTING_CHUNK = 100
@frappe.whitelist() @frappe.whitelist()
def get_fiscal_year( def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False 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 not fiscal_years:
# if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate) # if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate)
cond = "" FY = DocType("Fiscal Year")
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)
)
"""
fiscal_years = frappe.db.sql( query = (
""" frappe.qb.from_(FY)
select .select(FY.name, FY.year_start_date, FY.year_end_date)
fy.name, fy.year_start_date, fy.year_end_date .where(FY.disabled == 0)
from
`tabFiscal Year` fy
where
disabled = 0 {0}
order by
fy.year_start_date desc""".format(
cond
),
{"company": company},
as_dict=True,
) )
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) frappe.cache().hset("fiscal_years", company, fiscal_years)
if not transaction_date and not fiscal_year: if not transaction_date and not fiscal_year:
@ -423,7 +439,8 @@ def reconcile_against_document(args):
# cancel advance entry # cancel advance entry
doc = frappe.get_doc(voucher_type, voucher_no) doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True 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: for entry in entries:
check_if_advance_entry_modified(entry) check_if_advance_entry_modified(entry)
@ -438,7 +455,9 @@ def reconcile_against_document(args):
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
# re-submit advance entry # re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) 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 frappe.flags.ignore_party_validation = False
if entry.voucher_type in ("Payment Entry", "Journal Entry"): 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 select t2.{dr_or_cr} from `tabJournal Entry` t1, `tabJournal Entry Account` t2
where t1.name = t2.parent and t2.account = %(account)s where t1.name = t2.parent and t2.account = %(account)s
and t2.party_type = %(party_type)s and t2.party = %(party)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.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
and t1.docstatus=1 """.format( and t1.docstatus=1 """.format(
dr_or_cr=args.get("dr_or_cr") 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 t1.name = t2.parent and t1.docstatus = 1
and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s 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 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 and t2.allocated_amount = %(unreconciled_amount)s
""".format( """.format(
party_account_field party_account_field
@ -802,7 +821,11 @@ def get_held_invoices(party_type, party):
return held_invoices 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 = [] outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 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: else:
party_account_type = erpnext.get_party_account_type(party_type) 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) held_invoices = get_held_invoices(party_type, party)
invoice_list = frappe.db.sql( common_filter = common_filter or []
""" common_filter.append(ple.account_type == party_account_type)
select common_filter.append(ple.account == account)
voucher_no, voucher_type, posting_date, due_date, common_filter.append(ple.party_type == party_type)
ifnull(sum({dr_or_cr}), 0) as invoice_amount, common_filter.append(ple.party == party)
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,
)
payment_entries = frappe.db.sql( ple_query = QueryPaymentLedger()
""" invoice_list = ple_query.get_voucher_outstandings(
select against_voucher_type, against_voucher, common_filter=common_filter,
ifnull(sum({payment_dr_or_cr}), 0) as payment_amount min_outstanding=min_outstanding,
from `tabGL Entry` max_outstanding=max_outstanding,
where party_type = %(party_type)s and party = %(party)s get_invoices=True,
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,
) )
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: for d in invoice_list:
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0) payment_amount = d.invoice_amount - d.outstanding
outstanding_amount = flt(d.invoice_amount - payment_amount, precision) outstanding_amount = d.outstanding
if outstanding_amount > 0.5 / (10**precision): if outstanding_amount > 0.5 / (10**precision):
if ( if (
filters min_outstanding
and filters.get("outstanding_amt_greater_than") and max_outstanding
and not ( and not (outstanding_amount >= min_outstanding and outstanding_amount <= max_outstanding)
outstanding_amount >= filters.get("outstanding_amt_greater_than")
and outstanding_amount <= filters.get("outstanding_amt_less_than")
)
): ):
continue continue
@ -1122,7 +1099,11 @@ def update_gl_entries_after(
def repost_gle_for_stock_vouchers( 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 from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative
@ -1130,41 +1111,51 @@ def repost_gle_for_stock_vouchers(
if not stock_vouchers: if not stock_vouchers:
return 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: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) 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 precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) for stock_vouchers_chunk in create_batch(stock_vouchers, GL_REPOSTING_CHUNK):
for idx, (voucher_type, voucher_no) in enumerate(stock_vouchers): gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date)
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)
if idx % 20 == 0: for voucher_type, voucher_no in stock_vouchers_chunk:
# Commit every 20 documents to avoid losing progress existing_gle = gle.get((voucher_type, voucher_no), [])
# and reducing memory usage 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)
if not frappe.flags.in_test:
frappe.db.commit() 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( def sort_stock_vouchers_by_posting_date(
stock_vouchers: List[Tuple[str, str]] stock_vouchers: List[Tuple[str, str]]
@ -1177,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) .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))) .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no) .groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_date)
.orderby(sle.posting_time)
.orderby(sle.creation)
).run(as_dict=True) ).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
@ -1358,7 +1352,9 @@ def check_and_delete_linked_reports(report):
frappe.delete_doc("Desktop Icon", icon) 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: if gl_entries:
ple = None ple = None
@ -1431,9 +1427,42 @@ def create_payment_ledger_entry(gl_entries, cancel=0):
if cancel: if cancel:
delink_original_entry(ple) delink_original_entry(ple)
ple.flags.ignore_permissions = 1 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() 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): def delink_original_entry(pl_entry):
if pl_entry: if pl_entry:
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
@ -1455,3 +1484,196 @@ def delink_original_entry(pl_entry):
) )
) )
query.run() 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

@ -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") team_member = frappe.db.get_value("User", assign_to_member, "email")
args = { args = {
"doctype": "Asset Maintenance", "doctype": "Asset Maintenance",
"assign_to": [team_member], "assign_to": team_member,
"name": asset_maintenance_name, "name": asset_maintenance_name,
"description": maintenance_task, "description": maintenance_task,
"date": next_due_date, "date": next_due_date,
} }
if not frappe.db.sql( if not frappe.db.sql(
"""select owner from `tabToDo` """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""", and owner=%(assign_to)s""",
args, args,
): ):
# assign_to function expects a list
args["assign_to"] = [args["assign_to"]]
assign_to.add(args) assign_to.add(args)

View File

@ -330,7 +330,7 @@ class TestPurchaseOrder(FrappeTestCase):
else: else:
# update valid from # update valid from
frappe.db.sql( 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""", where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template}, {"item": item, "tax": tax_template},
) )

View File

@ -213,6 +213,7 @@
"width": "60px" "width": "60px"
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Stock UOM", "label": "Stock UOM",
@ -242,6 +243,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor", "fieldname": "conversion_factor",
"fieldtype": "Float", "fieldtype": "Float",
"label": "UOM Conversion Factor", "label": "UOM Conversion Factor",
@ -593,6 +595,7 @@
"label": "Billed, Received & Returned" "label": "Billed, Received & Returned"
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty", "fieldname": "stock_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Qty in Stock UOM", "label": "Qty in Stock UOM",
@ -851,7 +854,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-02-02 13:10:18.398976", "modified": "2022-06-17 05:29:40.602349",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "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` """select `tabContact`.name from `tabContact`, `tabDynamic Link`
where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s 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 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")}, {"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")},
) )

View File

@ -252,7 +252,7 @@ def get_mapped_pi_records():
ON pi_item.`purchase_order` = po.`name` ON pi_item.`purchase_order` = po.`name`
WHERE WHERE
pi_item.docstatus = 1 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 AND pi_item.po_detail IS NOT NULL
""" """
) )
@ -271,7 +271,7 @@ def get_mapped_pr_records():
pr.docstatus=1 pr.docstatus=1
AND pr.name=pr_item.parent AND pr.name=pr_item.parent
AND pr_item.purchase_order_item IS NOT NULL 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 WHERE
parent.docstatus = 1 parent.docstatus = 1
AND parent.name = child.parent AND parent.name = child.parent
AND parent.status not in ("Closed","Completed","Cancelled") AND parent.status not in ('Closed','Completed','Cancelled')
{conditions} {conditions}
GROUP BY GROUP BY
parent.name, child.item_code 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.controllers.sales_and_purchase_return import validate_return
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.setup.utils import get_exchange_rate 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.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import ( from erpnext.stock.get_item_details import (
_get_item_tax_template, _get_item_tax_template,
@ -548,6 +549,15 @@ class AccountsController(TransactionBase):
if ret.get("pricing_rules"): if ret.get("pricing_rules"):
self.apply_pricing_rule_on_items(item, ret) self.apply_pricing_rule_on_items(item, ret)
self.set_pricing_rule_details(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": if self.doctype == "Purchase Invoice":
self.set_expense_account(for_validate) self.set_expense_account(for_validate)
@ -2039,7 +2049,7 @@ def get_advance_journal_entries(
journal_entries = frappe.db.sql( journal_entries = frappe.db.sql(
""" """
select 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, t1.remark as remarks, t2.{0} as amount, t2.name as reference_row,
t2.reference_name as against_order, t2.exchange_rate t2.reference_name as against_order, t2.exchange_rate
from from
@ -2094,7 +2104,7 @@ def get_advance_payment_entries(
payment_entries_against_order = frappe.db.sql( payment_entries_against_order = frappe.db.sql(
""" """
select 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, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
t2.reference_name as against_order, t1.posting_date, t2.reference_name as against_order, t1.posting_date,
t1.{0} as currency, t1.{4} as exchange_rate t1.{0} as currency, t1.{4} as exchange_rate
@ -2114,7 +2124,7 @@ def get_advance_payment_entries(
if include_unallocated: if include_unallocated:
unallocated_payment_entries = frappe.db.sql( 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 remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency
from `tabPayment Entry` from `tabPayment Entry`
where where

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ class TestMpesaSettings(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.db.sql("delete from `tabMpesa Settings`") 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): def test_creation_of_payment_gateway(self):
mode_of_payment = create_mode_of_payment("Mpesa-_Test", payment_type="Phone") mode_of_payment = create_mode_of_payment("Mpesa-_Test", payment_type="Phone")

View File

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

View File

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

View File

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

View File

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

View File

@ -108,7 +108,7 @@ class TestLeaveApplication(unittest.TestCase):
def _clear_roles(self): def _clear_roles(self):
frappe.db.sql( frappe.db.sql(
"""delete from `tabHas Role` where parent in """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): def _clear_applications(self):

View File

@ -5,6 +5,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.query_builder import Order from frappe.query_builder import Order
from frappe.utils import getdate from frappe.utils import getdate
from pypika import functions as fn
def execute(filters=None): def execute(filters=None):
@ -110,7 +111,7 @@ def get_data(filters):
) )
.distinct() .distinct()
.where( .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))) & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2)))
& ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.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`") frappe.db.sql("delete from `tabVehicle Log`")
employee_id = frappe.db.sql( 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 self.employee_id = employee_id[0][0] if employee_id else None
if not self.employee_id: if not self.employee_id:

View File

@ -458,7 +458,7 @@ def get_salary_assignments(employee, payroll_period):
def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
total_given_benefit_amount = 0 total_given_benefit_amount = 0
query = """ query = """
select sum(sd.amount) as 'total_amount' select sum(sd.amount) as total_amount
from `tabSalary Slip` ss, `tabSalary Detail` sd from `tabSalary Slip` ss, `tabSalary Detail` sd
where ss.employee=%(employee)s where ss.employee=%(employee)s
and ss.docstatus = 1 and ss.name = sd.parent and ss.docstatus = 1 and ss.name = sd.parent

View File

@ -1305,7 +1305,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not field in searchfields 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 = {} or_cond_filters = {}
if txt: if txt:

View File

@ -1,6 +1,6 @@
{ {
"actions": [], "actions": [],
"autoname": "autoincrement", "autoname": "hash",
"creation": "2022-05-31 17:34:39.825537", "creation": "2022-05-31 17:34:39.825537",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
@ -46,10 +46,9 @@
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Update Batch", "name": "BOM Update Batch",
"naming_rule": "Autoincrement",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@ -849,7 +849,7 @@ def get_subitems(
FROM FROM
`tabBOM Item` bom_item `tabBOM Item` bom_item
JOIN `tabBOM` bom ON bom.name = bom_item.parent 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 LEFT JOIN `tabItem Default` item_default
ON item.name = item_default.parent and item_default.company = %(company)s ON item.name = item_default.parent and item_default.company = %(company)s
LEFT JOIN `tabUOM Conversion Detail` item_uom 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 select distinct so.name, so.transaction_date, so.customer, so.base_grand_total
from `tabSales Order` so, `tabSales Order Item` so_item from `tabSales Order` so, `tabSales Order Item` so_item
where so_item.parent = so.name 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.company = %(company)s
and so_item.qty > so_item.work_order_qty {so_filter} {item_filter} and so_item.qty > so_item.work_order_qty {so_filter} {item_filter}
and (exists (select name from `tabBOM` bom where {bom_item} and (exists (select name from `tabBOM` bom where {bom_item}

View File

@ -939,7 +939,7 @@ class WorkOrder(Document):
from `tabStock Entry` entry, `tabStock Entry Detail` detail from `tabStock Entry` entry, `tabStock Entry Detail` detail
where where
entry.work_order = %(name)s entry.work_order = %(name)s
and entry.purpose = "Material Transfer for Manufacture" and entry.purpose = 'Material Transfer for Manufacture'
and entry.docstatus = 1 and entry.docstatus = 1
and detail.parent = entry.name and detail.parent = entry.name
and (detail.item_code = %(item)s or detail.original_item = %(item)s)""", 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( return frappe.db.sql(
"""select name from `tabWork Order` """select name from `tabWork Order`
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1 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 cond, start, page_len
), ),
{"name": "%%%s%%" % txt}, {"name": "%%%s%%" % txt},

View File

@ -1,6 +1,6 @@
{ {
"charts": [], "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", "creation": "2020-03-02 17:11:37.032604",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@ -402,7 +402,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2022-05-31 22:08:19.408223", "modified": "2022-06-15 15:18:57.062935",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing", "name": "Manufacturing",
@ -415,39 +415,35 @@
"sequence_id": 17.0, "sequence_id": 17.0,
"shortcuts": [ "shortcuts": [
{ {
"color": "Green", "color": "Grey",
"format": "{} Active", "doc_view": "List",
"label": "Item",
"link_to": "Item",
"restrict_to_domain": "Manufacturing",
"stats_filter": "{\n \"disabled\": 0\n}",
"type": "DocType"
},
{
"color": "Green",
"format": "{} Active",
"label": "BOM", "label": "BOM",
"link_to": "BOM", "link_to": "BOM",
"restrict_to_domain": "Manufacturing", "stats_filter": "{\"is_active\":[\"=\",1]}",
"stats_filter": "{\n \"is_active\": 1\n}",
"type": "DocType" "type": "DocType"
}, },
{ {
"color": "Yellow", "color": "Grey",
"format": "{} Open", "doc_view": "List",
"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",
"label": "Production Plan", "label": "Production Plan",
"link_to": "Production Plan", "link_to": "Production Plan",
"restrict_to_domain": "Manufacturing", "stats_filter": "{\"status\":[\"not in\",[\"Closed\",\"Cancelled\",\"Completed\"]]}",
"stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}", "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" "type": "DocType"
}, },
{ {
@ -455,12 +451,6 @@
"link_to": "Exponential Smoothing Forecasting", "link_to": "Exponential Smoothing Forecasting",
"type": "Report" "type": "Report"
}, },
{
"label": "Work Order Summary",
"link_to": "Work Order Summary",
"restrict_to_domain": "Manufacturing",
"type": "Report"
},
{ {
"label": "BOM Stock Report", "label": "BOM Stock Report",
"link_to": "BOM Stock Report", "link_to": "BOM Stock Report",
@ -470,12 +460,6 @@
"label": "Production Planning Report", "label": "Production Planning Report",
"link_to": "Production Planning Report", "link_to": "Production Planning Report",
"type": "Report" "type": "Report"
},
{
"label": "Dashboard",
"link_to": "Manufacturing",
"restrict_to_domain": "Manufacturing",
"type": "Dashboard"
} }
], ],
"title": "Manufacturing" "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_healthcare_doctypes
erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022 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_education_doctypes
erpnext.patches.v14_0.delete_datev_doctypes erpnext.patches.v14_0.delete_datev_doctypes
erpnext.patches.v14_0.rearrange_company_fields 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") execute:frappe.delete_doc("DocType", "Naming Series")
erpnext.patches.v13_0.set_payroll_entry_status erpnext.patches.v13_0.set_payroll_entry_status
erpnext.patches.v13_0.job_card_status_on_hold 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()

View File

@ -1,8 +0,0 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
frappe.reload_doc("Healthcare", "doctype", "Inpatient Record")
if frappe.db.has_column("Inpatient Record", "discharge_date"):
rename_field("Inpatient Record", "discharge_date", "discharge_datetime")

View File

@ -1,25 +0,0 @@
import frappe
def execute():
company = frappe.db.get_single_value("Global Defaults", "default_company")
doctypes = [
"Clinical Procedure",
"Inpatient Record",
"Lab Test",
"Sample Collection",
"Patient Appointment",
"Patient Encounter",
"Vital Signs",
"Therapy Session",
"Therapy Plan",
"Patient Assessment",
]
for entry in doctypes:
if frappe.db.exists("DocType", entry):
frappe.reload_doc("Healthcare", "doctype", entry)
frappe.db.sql(
"update `tab{dt}` set company = {company} where ifnull(company, '') = ''".format(
dt=entry, company=frappe.db.escape(company)
)
)

View File

@ -2,6 +2,9 @@ import frappe
def execute(): def execute():
if "agriculture" in frappe.get_installed_apps():
return
frappe.delete_doc("Module Def", "Agriculture", ignore_missing=True, force=True) frappe.delete_doc("Module Def", "Agriculture", ignore_missing=True, force=True)
frappe.delete_doc("Workspace", "Agriculture", ignore_missing=True, force=True) frappe.delete_doc("Workspace", "Agriculture", ignore_missing=True, force=True)
@ -19,3 +22,5 @@ def execute():
doctypes = frappe.get_all("DocType", {"module": "agriculture", "custom": 0}, pluck="name") doctypes = frappe.get_all("DocType", {"module": "agriculture", "custom": 0}, pluck="name")
for doctype in doctypes: for doctype in doctypes:
frappe.delete_doc("DocType", doctype, ignore_missing=True) frappe.delete_doc("DocType", doctype, ignore_missing=True)
frappe.delete_doc("Module Def", "Agriculture", ignore_missing=True, force=True)

View File

@ -674,7 +674,7 @@ def get_filter_condition(filters):
def get_joining_relieving_condition(start_date, end_date): def get_joining_relieving_condition(start_date, end_date):
cond = """ cond = """
and ifnull(t1.date_of_joining, '0000-00-00') <= '%(end_date)s' and ifnull(t1.date_of_joining, '1900-01-01') <= '%(end_date)s'
and ifnull(t1.relieving_date, '2199-12-31') >= '%(start_date)s' and ifnull(t1.relieving_date, '2199-12-31') >= '%(start_date)s'
""" % { """ % {
"start_date": start_date, "start_date": start_date,
@ -970,7 +970,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
and name not in and name not in
(select reference_name from `tabJournal Entry Account` (select reference_name from `tabJournal Entry Account`
where reference_type="Payroll Entry") where reference_type="Payroll Entry")
order by name limit %(start)s, %(page_len)s""".format( order by name limit %(page_len)s offset %(start)s""".format(
key=searchfield key=searchfield
), ),
{"txt": "%%%s%%" % txt, "start": start, "page_len": page_len}, {"txt": "%%%s%%" % txt, "start": start, "page_len": page_len},
@ -1035,11 +1035,11 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
{emp_cond} {emp_cond}
{fcond} {mcond} {fcond} {mcond}
order by 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),
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), (case when locate(%(_txt)s, employee_name) > 0 then locate(%(_txt)s, employee_name) else 99999 end),
idx desc, idx desc,
name, employee_name name, employee_name
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
**{ **{
"key": searchfield, "key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions), "fcond": get_filters_cond(doctype, filters, conditions),

View File

@ -508,7 +508,7 @@ class SalarySlip(TransactionBase):
SELECT attendance_date, status, leave_type SELECT attendance_date, status, leave_type
FROM `tabAttendance` FROM `tabAttendance`
WHERE WHERE
status in ("Absent", "Half Day", "On leave") status in ('Absent', 'Half Day', 'On leave')
AND employee = %s AND employee = %s
AND docstatus = 1 AND docstatus = 1
AND attendance_date between %s and %s AND attendance_date between %s and %s

View File

@ -387,11 +387,11 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters):
or full_name like %(txt)s) or full_name like %(txt)s)
{fcond} {mcond} {fcond} {mcond}
order by 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),
if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999), (case when locate(%(_txt)s, full_name) > 0 then locate(%(_txt)s, full_name) else 99999 end)
idx desc, idx desc,
name, full_name name, full_name
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
**{ **{
"key": searchfield, "key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions), "fcond": get_filters_cond(doctype, filters, conditions),

View File

@ -28,7 +28,7 @@ def daily_reminder():
for drafts in draft: for drafts in draft:
number_of_drafts = drafts[0] number_of_drafts = drafts[0]
update = frappe.db.sql( update = frappe.db.sql(
"""SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURDATE(), INTERVAL -1 DAY);""", """SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURRENT_DATE, INTERVAL -1 DAY);""",
project_name, project_name,
) )
email_sending(project_name, frequency, date_start, date_end, progress, number_of_drafts, update) email_sending(project_name, frequency, date_start, date_end, progress, number_of_drafts, update)
@ -39,7 +39,7 @@ def email_sending(
): ):
holiday = frappe.db.sql( holiday = frappe.db.sql(
"""SELECT holiday_date FROM `tabHoliday` where holiday_date = CURDATE();""" """SELECT holiday_date FROM `tabHoliday` where holiday_date = CURRENT_DATE;"""
) )
msg = ( msg = (
"<p>Project Name: " "<p>Project Name: "

View File

@ -288,7 +288,7 @@ def get_project(doctype, txt, searchfield, start, page_len, filters):
%(mcond)s %(mcond)s
{search_condition} {search_condition}
order by name order by name
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
search_columns=search_columns, search_condition=search_cond search_columns=search_columns, search_condition=search_cond
), ),
{ {

View File

@ -328,7 +328,7 @@ def get_timesheet(doctype, txt, searchfield, start, page_len, filters):
ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and
tsd.docstatus = 1 and ts.total_billable_amount > 0 tsd.docstatus = 1 and ts.total_billable_amount > 0
and tsd.parent LIKE %(txt)s {condition} and tsd.parent LIKE %(txt)s {condition}
order by tsd.parent limit %(start)s, %(page_len)s""".format( order by tsd.parent limit %(page_len)s offset %(start)s""".format(
condition=condition condition=condition
), ),
{ {
@ -515,7 +515,7 @@ def get_timesheets_list(
tsd.project IN %(projects)s tsd.project IN %(projects)s
) )
ORDER BY `end_date` ASC ORDER BY `end_date` ASC
LIMIT {0}, {1} LIMIT {1} offset {0}
""".format( """.format(
limit_start, limit_page_length limit_start, limit_page_length
), ),

View File

@ -39,17 +39,17 @@ def get_rows(filters):
FROM FROM
(SELECT (SELECT
si.customer_name,si.base_grand_total, si.customer_name,si.base_grand_total,
si.name as voucher_no,tabTimesheet.employee, si.name as voucher_no,`tabTimesheet`.employee,
tabTimesheet.title as employee_name,tabTimesheet.parent_project as project, `tabTimesheet`.title as employee_name,`tabTimesheet`.parent_project as project,
tabTimesheet.start_date,tabTimesheet.end_date, `tabTimesheet`.start_date,`tabTimesheet`.end_date,
tabTimesheet.total_billed_hours,tabTimesheet.name as timesheet, `tabTimesheet`.total_billed_hours,`tabTimesheet`.name as timesheet,
ss.base_gross_pay,ss.total_working_days, ss.base_gross_pay,ss.total_working_days,
tabTimesheet.total_billed_hours/(ss.total_working_days * {0}) as utilization `tabTimesheet`.total_billed_hours/(ss.total_working_days * {0}) as utilization
FROM FROM
`tabSalary Slip Timesheet` as sst join `tabTimesheet` on tabTimesheet.name = sst.time_sheet `tabSalary Slip Timesheet` as sst join `tabTimesheet` on `tabTimesheet`.name = sst.time_sheet
join `tabSales Invoice Timesheet` as sit on sit.time_sheet = tabTimesheet.name join `tabSales Invoice Timesheet` as sit on sit.time_sheet = `tabTimesheet`.name
join `tabSales Invoice` as si on si.name = sit.parent and si.status != "Cancelled" join `tabSales Invoice` as si on si.name = sit.parent and si.status != 'Cancelled'
join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format( join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != 'Cancelled' """.format(
standard_working_hours standard_working_hours
) )
if conditions: if conditions:
@ -72,23 +72,25 @@ def get_conditions(filters):
conditions = [] conditions = []
if filters.get("company"): if filters.get("company"):
conditions.append("tabTimesheet.company={0}".format(frappe.db.escape(filters.get("company")))) conditions.append("`tabTimesheet`.company={0}".format(frappe.db.escape(filters.get("company"))))
if filters.get("start_date"): if filters.get("start_date"):
conditions.append("tabTimesheet.start_date>='{0}'".format(filters.get("start_date"))) conditions.append("`tabTimesheet`.start_date>='{0}'".format(filters.get("start_date")))
if filters.get("end_date"): if filters.get("end_date"):
conditions.append("tabTimesheet.end_date<='{0}'".format(filters.get("end_date"))) conditions.append("`tabTimesheet`.end_date<='{0}'".format(filters.get("end_date")))
if filters.get("customer_name"): if filters.get("customer_name"):
conditions.append("si.customer_name={0}".format(frappe.db.escape(filters.get("customer_name")))) conditions.append("si.customer_name={0}".format(frappe.db.escape(filters.get("customer_name"))))
if filters.get("employee"): if filters.get("employee"):
conditions.append("tabTimesheet.employee={0}".format(frappe.db.escape(filters.get("employee")))) conditions.append(
"`tabTimesheet`.employee={0}".format(frappe.db.escape(filters.get("employee")))
)
if filters.get("project"): if filters.get("project"):
conditions.append( conditions.append(
"tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project"))) "`tabTimesheet`.parent_project={0}".format(frappe.db.escape(filters.get("project")))
) )
conditions = " and ".join(conditions) conditions = " and ".join(conditions)

View File

@ -25,7 +25,7 @@ def query_task(doctype, txt, searchfield, start, page_len, filters):
case when `%s` like %s then 0 else 1 end, case when `%s` like %s then 0 else 1 end,
`%s`, `%s`,
subject subject
limit %s, %s""" limit %s offset %s"""
% (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"), % (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"),
(search_string, search_string, order_by_string, order_by_string, start, page_len), (search_string, search_string, order_by_string, order_by_string, page_len, start),
) )

View File

@ -453,7 +453,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
is_pos: cint(me.frm.doc.is_pos), is_pos: cint(me.frm.doc.is_pos),
is_return: cint(me.frm.doc.is_return), is_return: cint(me.frm.doc.is_return),
is_subcontracted: me.frm.doc.is_subcontracted, is_subcontracted: me.frm.doc.is_subcontracted,
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,
doctype: me.frm.doc.doctype, doctype: me.frm.doc.doctype,
name: me.frm.doc.name, name: me.frm.doc.name,

View File

@ -287,7 +287,7 @@ def get_regional_address_details(party_details, doctype, company):
return party_details return party_details
if ( if (
doctype in ("Sales Invoice", "Delivery Note", "Sales Order") doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation")
and party_details.company_gstin and party_details.company_gstin
and party_details.company_gstin[:2] != party_details.place_of_supply[:2] and party_details.company_gstin[:2] != party_details.place_of_supply[:2]
) or ( ) or (

View File

@ -83,7 +83,7 @@ def get_conditions(filters):
("gst_hsn_code", " and gst_hsn_code=%(gst_hsn_code)s"), ("gst_hsn_code", " and gst_hsn_code=%(gst_hsn_code)s"),
("company_gstin", " and company_gstin=%(company_gstin)s"), ("company_gstin", " and company_gstin=%(company_gstin)s"),
("from_date", " and posting_date >= %(from_date)s"), ("from_date", " and posting_date >= %(from_date)s"),
("to_date", "and posting_date <= %(to_date)s"), ("to_date", " and posting_date <= %(to_date)s"),
): ):
if filters.get(opts[0]): if filters.get(opts[0]):
conditions += opts[1] conditions += opts[1]

View File

@ -10,7 +10,7 @@ from frappe.utils.data import fmt_money
from frappe.utils.jinja import render_template from frappe.utils.jinja import render_template
from frappe.utils.pdf import get_pdf from frappe.utils.pdf import get_pdf
from frappe.utils.print_format import read_multi_pdf from frappe.utils.print_format import read_multi_pdf
from PyPDF2 import PdfFileWriter from PyPDF2 import PdfWriter
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
@ -47,7 +47,7 @@ def execute(filters=None):
s.name = gl.party s.name = gl.party
AND s.irs_1099 = 1 AND s.irs_1099 = 1
AND gl.fiscal_year = %(fiscal_year)s AND gl.fiscal_year = %(fiscal_year)s
AND gl.party_type = "Supplier" AND gl.party_type = 'Supplier'
AND gl.company = %(company)s AND gl.company = %(company)s
{conditions} {conditions}
@ -106,7 +106,7 @@ def irs_1099_print(filters):
columns, data = execute(filters) columns, data = execute(filters)
template = frappe.get_doc("Print Format", "IRS 1099 Form").html template = frappe.get_doc("Print Format", "IRS 1099 Form").html
output = PdfFileWriter() output = PdfWriter()
for row in data: for row in data:
row["fiscal_year"] = fiscal_year row["fiscal_year"] = fiscal_year

View File

@ -65,7 +65,7 @@ class VATAuditReport(object):
`tab{doctype}` `tab{doctype}`
WHERE WHERE
docstatus = 1 {where_conditions} docstatus = 1 {where_conditions}
and is_opening = "No" and is_opening = 'No'
ORDER BY ORDER BY
posting_date DESC posting_date DESC
""".format( """.format(

View File

@ -78,7 +78,7 @@ def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql( return frappe.db.sql(
"""select name, item_name, description from tabItem """select name, item_name, description from tabItem
where is_stock_item=0 and name not in (select name from `tabProduct Bundle`) where is_stock_item=0 and name not in (select name from `tabProduct Bundle`)
and %s like %s %s limit %s, %s""" and %s like %s %s limit %s offset %s"""
% (searchfield, "%s", get_match_cond(doctype), "%s", "%s"), % (searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
("%%%s%%" % txt, start, page_len), ("%%%s%%" % txt, page_len, start),
) )

View File

@ -20,6 +20,20 @@ frappe.ui.form.on('Quotation', {
frm.set_df_property('packed_items', 'cannot_add_rows', true); frm.set_df_property('packed_items', 'cannot_add_rows', true);
frm.set_df_property('packed_items', 'cannot_delete_rows', true); frm.set_df_property('packed_items', 'cannot_delete_rows', true);
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Company',
link_name: doc.company
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {
@ -70,7 +84,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
} }
} }
if(doc.docstatus == 1 && doc.status!=='Lost') { if(doc.docstatus == 1 && !(['Lost', 'Ordered']).includes(doc.status)) {
if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
cur_frm.add_custom_button(__('Sales Order'), cur_frm.add_custom_button(__('Sales Order'),
cur_frm.cscript['Make Sales Order'], __('Create')); cur_frm.cscript['Make Sales Order'], __('Create'));

View File

@ -296,7 +296,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name",
"fieldname": "col_break98", "fieldname": "col_break98",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"width": "50%" "width": "50%"
@ -316,7 +316,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name",
"fieldname": "customer_group", "fieldname": "customer_group",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
@ -897,7 +897,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Draft\nOpen\nReplied\nOrdered\nLost\nCancelled\nExpired", "options": "Draft\nOpen\nReplied\nPartially Ordered\nOrdered\nLost\nCancelled\nExpired",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"reqd": 1 "reqd": 1
@ -986,7 +986,7 @@
"idx": 82, "idx": 82,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-04-07 11:01:31.157084", "modified": "2022-06-11 20:35:32.635804",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation", "name": "Quotation",
@ -1084,4 +1084,4 @@
"states": [], "states": [],
"timeline_field": "party_name", "timeline_field": "party_name",
"title_field": "title" "title_field": "title"
} }

View File

@ -70,8 +70,32 @@ class Quotation(SellingController):
title=_("Unpublished Item"), title=_("Unpublished Item"),
) )
def has_sales_order(self): def get_ordered_status(self):
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1}) ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
{"prevdoc_docname": self.name, "docstatus": 1},
["item_code", "sum(qty)"],
group_by="item_code",
as_list=1,
)
)
status = "Open"
if ordered_items:
status = "Ordered"
for item in self.get("items"):
if item.qty > ordered_items.get(item.item_code, 0.0):
status = "Partially Ordered"
return status
def is_fully_ordered(self):
return self.get_ordered_status() == "Ordered"
def is_partially_ordered(self):
return self.get_ordered_status() == "Partially Ordered"
def update_lead(self): def update_lead(self):
if self.quotation_to == "Lead" and self.party_name: if self.quotation_to == "Lead" and self.party_name:
@ -103,7 +127,7 @@ class Quotation(SellingController):
@frappe.whitelist() @frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, competitors, detailed_reason=None): def declare_enquiry_lost(self, lost_reasons_list, competitors, detailed_reason=None):
if not self.has_sales_order(): if not (self.is_fully_ordered() or self.is_partially_ordered()):
get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"])
lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons]
frappe.db.set(self, "status", "Lost") frappe.db.set(self, "status", "Lost")
@ -243,7 +267,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
def set_expired_status(): def set_expired_status():
# filter out submitted non expired quotations whose validity has been ended # filter out submitted non expired quotations whose validity has been ended
cond = "qo.docstatus = 1 and qo.status != 'Expired' and qo.valid_till < %s" cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status != 'Expired' and `tabQuotation`.valid_till < %s"
# check if those QUO have SO against it # check if those QUO have SO against it
so_against_quo = """ so_against_quo = """
SELECT SELECT
@ -251,13 +275,18 @@ def set_expired_status():
WHERE WHERE
so_item.docstatus = 1 and so.docstatus = 1 so_item.docstatus = 1 and so.docstatus = 1
and so_item.parent = so.name and so_item.parent = so.name
and so_item.prevdoc_docname = qo.name""" and so_item.prevdoc_docname = `tabQuotation`.name"""
# if not exists any SO, set status as Expired # if not exists any SO, set status as Expired
frappe.db.sql( frappe.db.multisql(
"""UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""".format( {
cond=cond, so_against_quo=so_against_quo "mariadb": """UPDATE `tabQuotation` SET `tabQuotation`.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""".format(
), cond=cond, so_against_quo=so_against_quo
),
"postgres": """UPDATE `tabQuotation` SET status = 'Expired' FROM `tabSales Order`, `tabSales Order Item` WHERE {cond} and not exists({so_against_quo})""".format(
cond=cond, so_against_quo=so_against_quo
),
},
(nowdate()), (nowdate()),
) )

View File

@ -25,6 +25,8 @@ frappe.listview_settings['Quotation'] = {
get_indicator: function(doc) { get_indicator: function(doc) {
if(doc.status==="Open") { if(doc.status==="Open") {
return [__("Open"), "orange", "status,=,Open"]; return [__("Open"), "orange", "status,=,Open"];
} else if (doc.status==="Partially Ordered") {
return [__("Partially Ordered"), "yellow", "status,=,Partially Ordered"];
} else if(doc.status==="Ordered") { } else if(doc.status==="Ordered") {
return [__("Ordered"), "green", "status,=,Ordered"]; return [__("Ordered"), "green", "status,=,Ordered"];
} else if(doc.status==="Lost") { } else if(doc.status==="Lost") {

View File

@ -25,6 +25,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import get_default_bom
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
form_grid_templates = {"items": "templates/form_grid/item_grid.html"} form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@ -423,8 +424,9 @@ class SalesOrder(SellingController):
for table in [self.items, self.packed_items]: for table in [self.items, self.packed_items]:
for i in table: for i in table:
bom = get_default_bom_item(i.item_code) bom = get_default_bom(i.item_code)
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
if not for_raw_material_request: if not for_raw_material_request:
total_work_order_qty = flt( total_work_order_qty = flt(
frappe.db.sql( frappe.db.sql(
@ -438,32 +440,19 @@ class SalesOrder(SellingController):
pending_qty = stock_qty pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents: if pending_qty and i.item_code not in product_bundle_parents:
if bom: items.append(
items.append( dict(
dict( name=i.name,
name=i.name, item_code=i.item_code,
item_code=i.item_code, description=i.description,
description=i.description, bom=bom or "",
bom=bom, warehouse=i.warehouse,
warehouse=i.warehouse, pending_qty=pending_qty,
pending_qty=pending_qty, required_qty=pending_qty if for_raw_material_request else 0,
required_qty=pending_qty if for_raw_material_request else 0, sales_order_item=i.name,
sales_order_item=i.name,
)
)
else:
items.append(
dict(
name=i.name,
item_code=i.item_code,
description=i.description,
bom="",
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,
sales_order_item=i.name,
)
) )
)
return items return items
def on_recurring(self, reference_doc, auto_repeat_doc): def on_recurring(self, reference_doc, auto_repeat_doc):
@ -1167,13 +1156,6 @@ def update_status(status, name):
so.update_status(status) so.update_status(status)
def get_default_bom_item(item_code):
bom = frappe.get_all("BOM", dict(item=item_code, is_active=True), order_by="is_default desc")
bom = bom[0].name if bom else None
return bom
@frappe.whitelist() @frappe.whitelist()
def make_raw_material_request(items, company, sales_order, project=None): def make_raw_material_request(items, company, sales_order, project=None):
if not frappe.has_permission("Sales Order", "write"): if not frappe.has_permission("Sales Order", "write"):

View File

@ -329,7 +329,7 @@ class TestSalesOrder(FrappeTestCase):
def test_sales_order_on_hold(self): def test_sales_order_on_hold(self):
so = make_sales_order(item_code="_Test Product Bundle Item") so = make_sales_order(item_code="_Test Product Bundle Item")
so.db_set("Status", "On Hold") so.db_set("status", "On Hold")
si = make_sales_invoice(so.name) si = make_sales_invoice(so.name)
self.assertRaises(frappe.ValidationError, create_dn_against_so, so.name) self.assertRaises(frappe.ValidationError, create_dn_against_so, so.name)
self.assertRaises(frappe.ValidationError, si.submit) self.assertRaises(frappe.ValidationError, si.submit)
@ -644,7 +644,7 @@ class TestSalesOrder(FrappeTestCase):
else: else:
# update valid from # update valid from
frappe.db.sql( 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""", where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template}, {"item": item, "tax": tax_template},
) )
@ -1380,6 +1380,59 @@ class TestSalesOrder(FrappeTestCase):
except Exception: except Exception:
self.fail("Can not cancel sales order with linked cancelled payment entry") self.fail("Can not cancel sales order with linked cancelled payment entry")
def test_work_order_pop_up_from_sales_order(self):
"Test `get_work_order_items` in Sales Order picks the right BOM for items to manufacture."
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
make_item( # template item
"Test-WO-Tshirt",
{
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [{"attribute": "Test Colour"}],
},
)
make_item("Test-RM-Cotton") # RM for BOM
for colour in (
"Red",
"Green",
):
variant = create_variant("Test-WO-Tshirt", {"Test Colour": colour})
variant.save()
template_bom = make_bom(item="Test-WO-Tshirt", rate=100, raw_materials=["Test-RM-Cotton"])
red_var_bom = make_bom(item="Test-WO-Tshirt-R", rate=100, raw_materials=["Test-RM-Cotton"])
so = make_sales_order(
**{
"item_list": [
{
"item_code": "Test-WO-Tshirt-R",
"qty": 1,
"rate": 1000,
"warehouse": "_Test Warehouse - _TC",
},
{
"item_code": "Test-WO-Tshirt-G",
"qty": 1,
"rate": 1000,
"warehouse": "_Test Warehouse - _TC",
},
]
}
)
wo_items = so.get_work_order_items()
self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
# Must pick Template Item BOM for Test-WO-Tshirt-G as it has no BOM
self.assertEqual(wo_items[1].get("item_code"), "Test-WO-Tshirt-G")
self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self): def test_request_for_raw_materials(self):
item = make_item( item = make_item(
"_Test Finished Item", "_Test Finished Item",

View File

@ -23,7 +23,6 @@
"quantity_and_rate", "quantity_and_rate",
"qty", "qty",
"stock_uom", "stock_uom",
"picked_qty",
"col_break2", "col_break2",
"uom", "uom",
"conversion_factor", "conversion_factor",
@ -87,6 +86,7 @@
"delivered_qty", "delivered_qty",
"produced_qty", "produced_qty",
"returned_qty", "returned_qty",
"picked_qty",
"shopping_cart_section", "shopping_cart_section",
"additional_notes", "additional_notes",
"section_break_63", "section_break_63",
@ -198,6 +198,7 @@
"width": "100px" "width": "100px"
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Stock UOM", "label": "Stock UOM",
@ -220,6 +221,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor", "fieldname": "conversion_factor",
"fieldtype": "Float", "fieldtype": "Float",
"label": "UOM Conversion Factor", "label": "UOM Conversion Factor",
@ -228,6 +230,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty", "fieldname": "stock_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Qty as per Stock UOM", "label": "Qty as per Stock UOM",
@ -811,7 +814,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-04-27 03:15:34.366563", "modified": "2022-06-17 05:27:41.603006",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@ -107,7 +107,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
ORDER BY ORDER BY
item.name asc item.name asc
LIMIT LIMIT
{start}, {page_length}""".format( {page_length} offset {start}""".format(
start=start, start=start,
page_length=page_length, page_length=page_length,
lft=lft, lft=lft,
@ -204,7 +204,7 @@ def item_group_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql( return frappe.db.sql(
""" select distinct name from `tabItem Group` """ select distinct name from `tabItem Group`
where {condition} and (name like %(txt)s) limit {start}, {page_len}""".format( where {condition} and (name like %(txt)s) limit {page_len} offset {start}""".format(
condition=cond, start=start, page_len=page_len condition=cond, start=start, page_len=page_len
), ),
{"txt": "%%%s%%" % txt}, {"txt": "%%%s%%" % txt},

View File

@ -31,13 +31,13 @@ def execute(filters=None):
def get_sales_details(doctype): def get_sales_details(doctype):
cond = """sum(so.base_net_total) as 'total_order_considered', cond = """sum(so.base_net_total) as 'total_order_considered',
max(so.posting_date) as 'last_order_date', max(so.posting_date) as 'last_order_date',
DATEDIFF(CURDATE(), max(so.posting_date)) as 'days_since_last_order' """ DATEDIFF(CURRENT_DATE, max(so.posting_date)) as 'days_since_last_order' """
if doctype == "Sales Order": if doctype == "Sales Order":
cond = """sum(if(so.status = "Stopped", cond = """sum(if(so.status = "Stopped",
so.base_net_total * so.per_delivered/100, so.base_net_total * so.per_delivered/100,
so.base_net_total)) as 'total_order_considered', so.base_net_total)) as 'total_order_considered',
max(so.transaction_date) as 'last_order_date', max(so.transaction_date) as 'last_order_date',
DATEDIFF(CURDATE(), max(so.transaction_date)) as 'days_since_last_order'""" DATEDIFF(CURRENT_DATE, max(so.transaction_date)) as 'days_since_last_order'"""
return frappe.db.sql( return frappe.db.sql(
"""select """select

View File

@ -65,7 +65,7 @@ def get_data():
WHERE WHERE
so.docstatus = 1 so.docstatus = 1
and so.name = so_item.parent and so.name = so_item.parent
and so.status not in ("Closed","Completed","Cancelled") and so.status not in ('Closed','Completed','Cancelled')
GROUP BY GROUP BY
so.name,so_item.item_code so.name,so_item.item_code
""", """,

View File

@ -64,7 +64,7 @@ def get_data(conditions, filters):
soi.delivery_date as delivery_date, soi.delivery_date as delivery_date,
so.name as sales_order, so.name as sales_order,
so.status, so.customer, soi.item_code, so.status, so.customer, soi.item_code,
DATEDIFF(CURDATE(), soi.delivery_date) as delay_days, DATEDIFF(CURRENT_DATE, soi.delivery_date) as delay_days,
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
soi.qty, soi.delivered_qty, soi.qty, soi.delivered_qty,
(soi.qty - soi.delivered_qty) AS pending_qty, (soi.qty - soi.delivered_qty) AS pending_qty,

View File

@ -464,7 +464,7 @@ class Company(NestedSet):
# reset default company # reset default company
frappe.db.sql( frappe.db.sql(
"""update `tabSingles` set value="" """update `tabSingles` set value=''
where doctype='Global Defaults' and field='default_company' where doctype='Global Defaults' and field='default_company'
and value=%s""", and value=%s""",
self.name, self.name,
@ -472,7 +472,7 @@ class Company(NestedSet):
# reset default company # reset default company
frappe.db.sql( frappe.db.sql(
"""update `tabSingles` set value="" """update `tabSingles` set value=''
where doctype='Chart of Accounts Importer' and field='company' where doctype='Chart of Accounts Importer' and field='company'
and value=%s""", and value=%s""",
self.name, self.name,

View File

@ -198,7 +198,7 @@ class EmailDigest(Document):
todo_list = frappe.db.sql( todo_list = frappe.db.sql(
"""select * """select *
from `tabToDo` where (owner=%s or assigned_by=%s) and status="Open" from `tabToDo` where (owner=%s or assigned_by=%s) and status='Open'
order by field(priority, 'High', 'Medium', 'Low') asc, date asc limit 20""", order by field(priority, 'High', 'Medium', 'Low') asc, date asc limit 20""",
(user_id, user_id), (user_id, user_id),
as_dict=True, as_dict=True,
@ -854,7 +854,7 @@ class EmailDigest(Document):
sql_po = """select {fields} from `tabPurchase Order Item` sql_po = """select {fields} from `tabPurchase Order Item`
left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent
where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date
and received_qty < qty order by `tabPurchase Order Item`.parent DESC, and received_qty < qty order by `tabPurchase Order Item`.parent DESC,
`tabPurchase Order Item`.schedule_date DESC""".format( `tabPurchase Order Item`.schedule_date DESC""".format(
fields=fields_po fields=fields_po
@ -862,7 +862,7 @@ class EmailDigest(Document):
sql_poi = """select {fields} from `tabPurchase Order Item` sql_poi = """select {fields} from `tabPurchase Order Item`
left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent
where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date
and received_qty < qty order by `tabPurchase Order Item`.idx""".format( and received_qty < qty order by `tabPurchase Order Item`.idx""".format(
fields=fields_poi fields=fields_poi
) )

View File

@ -21,7 +21,7 @@ def get_party_type(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql( return frappe.db.sql(
"""select name from `tabParty Type` """select name from `tabParty Type`
where `{key}` LIKE %(txt)s {cond} where `{key}` LIKE %(txt)s {cond}
order by name limit %(start)s, %(page_len)s""".format( order by name limit %(page_len)s offset %(start)s""".format(
key=searchfield, cond=cond key=searchfield, cond=cond
), ),
{"txt": "%" + txt + "%", "start": start, "page_len": page_len}, {"txt": "%" + txt + "%", "start": start, "page_len": page_len},

View File

@ -42,7 +42,7 @@ class TransactionDeletionRecord(Document):
def delete_bins(self): def delete_bins(self):
frappe.db.sql( frappe.db.sql(
"""delete from tabBin where warehouse in """delete from `tabBin` where warehouse in
(select name from tabWarehouse where company=%s)""", (select name from tabWarehouse where company=%s)""",
self.company, self.company,
) )
@ -64,7 +64,7 @@ class TransactionDeletionRecord(Document):
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
frappe.db.sql( frappe.db.sql(
"""delete from tabAddress where name in ({addresses}) and """delete from `tabAddress` where name in ({addresses}) and
name not in (select distinct dl1.parent from `tabDynamic Link` dl1 name not in (select distinct dl1.parent from `tabDynamic Link` dl1
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
and dl1.link_doctype<>dl2.link_doctype)""".format( and dl1.link_doctype<>dl2.link_doctype)""".format(
@ -80,7 +80,7 @@ class TransactionDeletionRecord(Document):
) )
frappe.db.sql( frappe.db.sql(
"""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format( """update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
leads=",".join(leads) leads=",".join(leads)
) )
) )
@ -178,7 +178,7 @@ class TransactionDeletionRecord(Document):
else: else:
last = 0 last = 0
frappe.db.sql("""update tabSeries set current = %s where name=%s""", (last, prefix)) frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix))
def delete_version_log(self, doctype, company_fieldname): def delete_version_log(self, doctype, company_fieldname):
frappe.db.sql( frappe.db.sql(

View File

@ -335,7 +335,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
and `tabStock Ledger Entry`.is_cancelled = 0 and `tabStock Ledger Entry`.is_cancelled = 0
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} and (`tabBatch`.expiry_date >= CURRENT_DATE or `tabBatch`.expiry_date IS NULL) {0}
group by batch_id group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
""".format( """.format(

View File

@ -184,6 +184,7 @@
"width": "100px" "width": "100px"
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Stock UOM", "label": "Stock UOM",
@ -209,6 +210,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor", "fieldname": "conversion_factor",
"fieldtype": "Float", "fieldtype": "Float",
"label": "UOM Conversion Factor", "label": "UOM Conversion Factor",
@ -217,6 +219,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty", "fieldname": "stock_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Qty in Stock UOM", "label": "Qty in Stock UOM",
@ -780,7 +783,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-05-02 12:09:39.610075", "modified": "2022-06-17 05:25:47.711177",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@ -263,9 +263,9 @@ def get_default_contact(out, name):
FROM FROM
`tabDynamic Link` dl `tabDynamic Link` dl
WHERE WHERE
dl.link_doctype="Customer" dl.link_doctype='Customer'
AND dl.link_name=%s AND dl.link_name=%s
AND dl.parenttype = "Contact" AND dl.parenttype = 'Contact'
""", """,
(name), (name),
as_dict=1, as_dict=1,
@ -289,9 +289,9 @@ def get_default_address(out, name):
FROM FROM
`tabDynamic Link` dl `tabDynamic Link` dl
WHERE WHERE
dl.link_doctype="Customer" dl.link_doctype='Customer'
AND dl.link_name=%s AND dl.link_name=%s
AND dl.parenttype = "Address" AND dl.parenttype = 'Address'
""", """,
(name), (name),
as_dict=1, as_dict=1,
@ -388,7 +388,7 @@ def notify_customers(delivery_trip):
if email_recipients: if email_recipients:
frappe.msgprint(_("Email sent to {0}").format(", ".join(email_recipients))) frappe.msgprint(_("Email sent to {0}").format(", ".join(email_recipients)))
delivery_trip.db_set("email_notification_sent", True) delivery_trip.db_set("email_notification_sent", 1)
else: else:
frappe.msgprint(_("No contacts with email IDs found.")) frappe.msgprint(_("No contacts with email IDs found."))

View File

@ -14,7 +14,6 @@
"details", "details",
"naming_series", "naming_series",
"item_code", "item_code",
"variant_of",
"item_name", "item_name",
"item_group", "item_group",
"stock_uom", "stock_uom",
@ -22,6 +21,7 @@
"disabled", "disabled",
"allow_alternative_item", "allow_alternative_item",
"is_stock_item", "is_stock_item",
"has_variants",
"include_item_in_manufacturing", "include_item_in_manufacturing",
"opening_stock", "opening_stock",
"valuation_rate", "valuation_rate",
@ -66,7 +66,7 @@
"has_serial_no", "has_serial_no",
"serial_no_series", "serial_no_series",
"variants_section", "variants_section",
"has_variants", "variant_of",
"variant_based_on", "variant_based_on",
"attributes", "attributes",
"accounting", "accounting",
@ -112,8 +112,8 @@
"quality_inspection_template", "quality_inspection_template",
"inspection_required_before_delivery", "inspection_required_before_delivery",
"manufacturing", "manufacturing",
"default_bom",
"is_sub_contracted_item", "is_sub_contracted_item",
"default_bom",
"column_break_74", "column_break_74",
"customer_code", "customer_code",
"default_item_manufacturer", "default_item_manufacturer",
@ -479,7 +479,7 @@
"collapsible_depends_on": "attributes", "collapsible_depends_on": "attributes",
"depends_on": "eval:!doc.is_fixed_asset", "depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "variants_section", "fieldname": "variants_section",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Variants" "label": "Variants"
}, },
{ {
@ -504,7 +504,8 @@
"fieldname": "attributes", "fieldname": "attributes",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 1, "hidden": 1,
"label": "Attributes", "label": "Variant Attributes",
"mandatory_depends_on": "has_variants",
"options": "Item Variant Attribute" "options": "Item Variant Attribute"
}, },
{ {
@ -909,7 +910,7 @@
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-06-08 11:35:20.094546", "modified": "2022-06-15 09:02:06.177691",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -1155,7 +1155,7 @@ def check_stock_uom_with_bin(item, stock_uom):
bin_list = frappe.db.sql( bin_list = frappe.db.sql(
""" """
select * from tabBin where item_code = %s select * from `tabBin` where item_code = %s
and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0) and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0)
and stock_uom != %s and stock_uom != %s
""", """,
@ -1171,7 +1171,7 @@ def check_stock_uom_with_bin(item, stock_uom):
) )
# No SLE or documents against item. Bin UOM can be changed safely. # No SLE or documents against item. Bin UOM can be changed safely.
frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) frappe.db.sql("""update `tabBin` set stock_uom=%s where item_code=%s""", (stock_uom, item))
def get_item_defaults(item_code, company): def get_item_defaults(item_code, company):

View File

@ -381,8 +381,8 @@ class TestItem(FrappeTestCase):
frappe.delete_doc_if_exists("Item Attribute", "Test Item Length") frappe.delete_doc_if_exists("Item Attribute", "Test Item Length")
frappe.db.sql( frappe.db.sql(
'''delete from `tabItem Variant Attribute` """delete from `tabItem Variant Attribute`
where attribute="Test Item Length"''' where attribute='Test Item Length' """
) )
frappe.flags.attribute_values = None frappe.flags.attribute_values = None
@ -800,6 +800,7 @@ def create_item(
item_code, item_code,
is_stock_item=1, is_stock_item=1,
valuation_rate=0, valuation_rate=0,
stock_uom="Nos",
warehouse="_Test Warehouse - _TC", warehouse="_Test Warehouse - _TC",
is_customer_provided_item=None, is_customer_provided_item=None,
customer=None, customer=None,
@ -815,6 +816,7 @@ def create_item(
item.item_name = item_code item.item_name = item_code
item.description = item_code item.description = item_code
item.item_group = "All Item Groups" item.item_group = "All Item Groups"
item.stock_uom = stock_uom
item.is_stock_item = is_stock_item item.is_stock_item = is_stock_item
item.is_fixed_asset = is_fixed_asset item.is_fixed_asset = is_fixed_asset
item.asset_category = asset_category item.asset_category = asset_category

View File

@ -77,7 +77,7 @@ def get_alternative_items(doctype, txt, searchfield, start, page_len, filters):
union union
(select item_code from `tabItem Alternative` (select item_code from `tabItem Alternative`
where alternative_item_code = %(item_code)s and item_code like %(txt)s where alternative_item_code = %(item_code)s and item_code like %(txt)s
and two_way = 1) limit {0}, {1} and two_way = 1) limit {1} offset {0}
""".format( """.format(
start, page_len start, page_len
), ),

View File

@ -24,7 +24,7 @@ class TestLandedCostVoucher(FrappeTestCase):
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
get_multiple_items=True, get_multiple_items=True,
get_taxes_and_charges=True, get_taxes_and_charges=True,
) )
@ -195,7 +195,7 @@ class TestLandedCostVoucher(FrappeTestCase):
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
get_multiple_items=True, get_multiple_items=True,
get_taxes_and_charges=True, get_taxes_and_charges=True,
do_not_submit=True, do_not_submit=True,
@ -280,7 +280,7 @@ class TestLandedCostVoucher(FrappeTestCase):
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
do_not_save=True, do_not_save=True,
) )
pr.items[0].cost_center = "Main - TCP1" pr.items[0].cost_center = "Main - TCP1"

View File

@ -203,7 +203,7 @@ def item_details(doctype, txt, searchfield, start, page_len, filters):
where name in ( select item_code FROM `tabDelivery Note Item` where name in ( select item_code FROM `tabDelivery Note Item`
where parent= %s) where parent= %s)
and %s like "%s" %s and %s like "%s" %s
limit %s, %s """ limit %s offset %s """
% ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"), % ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len), ((filters or {}).get("delivery_note"), "%%%s%%" % txt, page_len, start),
) )

View File

@ -699,7 +699,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
AND `company` = %(company)s AND `company` = %(company)s
AND `name` like %(txt)s AND `name` like %(txt)s
ORDER BY ORDER BY
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), name (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end) name
LIMIT LIMIT
%(start)s, %(page_length)s""", %(start)s, %(page_length)s""",
{ {

View File

@ -276,7 +276,7 @@ class TestPurchaseReceipt(FrappeTestCase):
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
get_multiple_items=True, get_multiple_items=True,
get_taxes_and_charges=True, get_taxes_and_charges=True,
) )
@ -486,13 +486,13 @@ class TestPurchaseReceipt(FrappeTestCase):
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
) )
return_pr = make_purchase_receipt( return_pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
is_return=1, is_return=1,
return_against=pr.name, return_against=pr.name,
qty=-2, qty=-2,
@ -573,13 +573,13 @@ class TestPurchaseReceipt(FrappeTestCase):
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
) )
return_pr = make_purchase_receipt( return_pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
is_return=1, is_return=1,
return_against=pr.name, return_against=pr.name,
qty=-5, qty=-5,
@ -615,7 +615,7 @@ class TestPurchaseReceipt(FrappeTestCase):
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
qty=2, qty=2,
rejected_qty=2, rejected_qty=2,
rejected_warehouse=rejected_warehouse, rejected_warehouse=rejected_warehouse,
@ -624,7 +624,7 @@ class TestPurchaseReceipt(FrappeTestCase):
return_pr = make_purchase_receipt( return_pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
is_return=1, is_return=1,
return_against=pr.name, return_against=pr.name,
qty=-2, qty=-2,
@ -951,7 +951,7 @@ class TestPurchaseReceipt(FrappeTestCase):
cost_center=cost_center, cost_center=cost_center,
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
) )
stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
@ -975,7 +975,7 @@ class TestPurchaseReceipt(FrappeTestCase):
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
supplier_warehouse="Work in Progress - TCP1", supplier_warehouse="Work In Progress - TCP1",
) )
stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)

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