Merge branch 'develop' into uae_setup_cleaup

This commit is contained in:
Deepesh Garg 2022-04-30 19:54:40 +05:30 committed by GitHub
commit bfd053ed23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1356 additions and 528 deletions
.github
README.mdTRADEMARK_POLICY.md
erpnext

@ -2,6 +2,13 @@
set -e set -e
# Check for merge conflicts before proceeding
python -m compileall -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
cd ~ || exit cd ~ || exit
sudo apt-get install redis-server libcups2-dev sudo apt-get install redis-server libcups2-dev

@ -1,4 +1,4 @@
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 2 193 52">
<g filter="url(#filter0_dd)"> <g filter="url(#filter0_dd)">
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/> <rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/> <path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>

Before

(image error) Size: 4.4 KiB

After

(image error) Size: 4.3 KiB

@ -1,5 +1,7 @@
<div align="center"> <div align="center">
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128"> <a href="https://erpnext.com">
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
</a>
<h2>ERPNext</h2> <h2>ERPNext</h2>
<p align="center"> <p align="center">
<p>ERP made simple</p> <p>ERP made simple</p>
@ -32,40 +34,39 @@ ERPNext as a monolith includes the following areas for managing businesses:
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext) 1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
1. [And More](https://erpnext.com/docs/user/manual/en/) 1. [And More](https://erpnext.com/docs/user/manual/en/)
ERPNext requires MariaDB.
ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript. ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
- [User Guide](https://erpnext.com/docs/user) ## Installation
- [Discussion Forum](https://discuss.erpnext.com/)
--- <div align="center" style="max-height: 40px;">
<a href="https://frappecloud.com/erpnext/signup">
<div align="center">
<a href="https://frappecloud.com/deploy?apps=frappe,erpnext&source=erpnext_readme">
<img src=".github/try-on-f-cloud-button.svg" height="40"> <img src=".github/try-on-f-cloud-button.svg" height="40">
</a> </a>
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD" height="37"/>
</a>
</div> </div>
> Login for the PWD site: (username: Administrator, password: admin)
### Containerized Installation ### Containerized Installation
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details. Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
### Full Install ### Manual Install
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details. The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt). New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
---
## License ## Learning and community
GNU/General Public License (see [license.txt](license.txt)) 1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
---
## Contributing ## Contributing
@ -73,49 +74,14 @@ The ERPNext code is licensed as GNU General Public License (v3) and the Document
1. [Report Security Vulnerabilities](https://erpnext.com/security) 1. [Report Security Vulnerabilities](https://erpnext.com/security)
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) 1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
1. [Translations](https://translate.erpnext.com) 1. [Translations](https://translate.erpnext.com)
1. [Chart of Accounts](https://charts.erpnext.com)
---
## Learning ## License
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. GNU/General Public License (see [license.txt](license.txt))
--- The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
## Logo and Trademark ## Logo and Trademark Policy
The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd. Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
### Introduction
Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind:
- Wed like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
- Wed like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
- Wed like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources.
### Frappe Trademark Usage Policy
Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name.
We will grant permission to use the ERPNext name and logo for projects that meet the following criteria:
- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
Use of the ERPNext name and logo is additionally allowed in the following situations:
All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.”
Similarly, its OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name.
We do not allow the use of the trademark in advertising, including AdSense/AdWords.
Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them.
When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification.
(inspired by WordPress)

36
TRADEMARK_POLICY.md Normal file

@ -0,0 +1,36 @@
## Logo and Trademark Policy
The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd.
### Introduction
Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind:
- Wed like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
- Wed like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
- Wed like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources.
### Frappe Trademark Usage Policy
Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name.
We will grant permission to use the ERPNext name and logo for projects that meet the following criteria:
- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
Use of the ERPNext name and logo is additionally allowed in the following situations:
All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.”
Similarly, its OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name.
We do not allow the use of the trademark in advertising, including AdSense/AdWords.
Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them.
When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification.
(inspired by WordPress)

@ -99,7 +99,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
if doctype == "Budget": if doctype == "Budget":
add_dimension_to_budget_doctype(df.copy(), doc) add_dimension_to_budget_doctype(df.copy(), doc)
else: else:
create_custom_field(doctype, df) create_custom_field(doctype, df, ignore_validate=True)
count += 1 count += 1
@ -115,7 +115,7 @@ def add_dimension_to_budget_doctype(df, doc):
} }
) )
create_custom_field("Budget", df) create_custom_field("Budget", df, ignore_validate=True)
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options") property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")

@ -18,7 +18,6 @@
"automatically_fetch_payment_terms", "automatically_fetch_payment_terms",
"column_break_17", "column_break_17",
"enable_common_party_accounting", "enable_common_party_accounting",
"enable_discount_accounting",
"report_setting_section", "report_setting_section",
"use_custom_cash_flow", "use_custom_cash_flow",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
@ -272,13 +271,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Create Ledger Entries for Change Amount" "label": "Create Ledger Entries for Change Amount"
}, },
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting"
},
{ {
"default": "0", "default": "0",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>", "description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
@ -354,7 +346,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-02-04 12:32:36.805652", "modified": "2022-04-08 14:45:06.796418",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

@ -28,7 +28,6 @@ class AccountsSettings(Document):
self.validate_stale_days() self.validate_stale_days()
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
self.validate_pending_reposts() self.validate_pending_reposts()
def validate_stale_days(self): def validate_stale_days(self):
@ -52,74 +51,6 @@ class AccountsSettings(Document):
validate_fields_for_doctype=False, validate_fields_for_doctype=False,
) )
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
make_property_setter(
doctype,
"discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
doctype,
"discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
doctype,
"discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
for doctype in ["Sales Invoice", "Purchase Invoice"]:
make_property_setter(
doctype,
"additional_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
doctype,
"additional_discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
doctype,
"additional_discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
make_property_setter(
"Item",
"default_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
def validate_pending_reposts(self): def validate_pending_reposts(self):
if self.acc_frozen_upto: if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto) check_pending_reposting(self.acc_frozen_upto)

@ -5,7 +5,10 @@
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, fmt_money, getdate, nowdate from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
import erpnext
form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"} form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
@ -76,6 +79,52 @@ class BankClearance(Document):
as_dict=1, as_dict=1,
) )
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
loan_disbursements = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document"),
loan_disbursement.name.as_("payment_entry"),
loan_disbursement.disbursed_amount.as_("credit"),
ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, frappe.qb.desc)
).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
loan_repayments = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document"),
loan_repayment.name.as_("payment_entry"),
loan_repayment.amount_paid.as_("debit"),
ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"),
loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
.orderby(loan_repayment.posting_date)
.orderby(loan_repayment.name, frappe.qb.desc)
).run(as_dict=1)
pos_sales_invoices, pos_purchase_invoices = [], [] pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions: if self.include_pos_transactions:
pos_sales_invoices = frappe.db.sql( pos_sales_invoices = frappe.db.sql(
@ -114,20 +163,29 @@ class BankClearance(Document):
entries = sorted( entries = sorted(
list(payment_entries) list(payment_entries)
+ list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), + list(journal_entries)
key=lambda k: k["posting_date"] or getdate(nowdate()), + list(pos_sales_invoices)
+ list(pos_purchase_invoices)
+ list(loan_disbursements)
+ list(loan_repayments),
key=lambda k: getdate(k["posting_date"]),
) )
self.set("payment_entries", []) self.set("payment_entries", [])
self.total_amount = 0.0 self.total_amount = 0.0
default_currency = erpnext.get_default_currency()
for d in entries: for d in entries:
row = self.append("payment_entries", {}) row = self.append("payment_entries", {})
amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0)) amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
if not d.get("account_currency"):
d.account_currency = default_currency
formatted_amount = fmt_money(abs(amount), 2, d.account_currency) formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
d.posting_date = getdate(d.posting_date)
d.pop("credit") d.pop("credit")
d.pop("debit") d.pop("debit")

@ -1,9 +1,96 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
# import frappe
import unittest import unittest
import frappe
from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_accounts,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
class TestBankClearance(unittest.TestCase): class TestBankClearance(unittest.TestCase):
pass @classmethod
def setUpClass(cls):
make_bank_account()
create_loan_accounts()
create_loan_masters()
add_transactions()
# Basic test case to test if bank clearance tool doesn't break
# Detailed test can be added later
def test_bank_clearance(self):
bank_clearance = frappe.get_doc("Bank Clearance")
bank_clearance.account = "_Test Bank Clearance - _TC"
bank_clearance.from_date = add_months(getdate(), -1)
bank_clearance.to_date = getdate()
bank_clearance.get_payment_entries()
self.assertEqual(len(bank_clearance.payment_entries), 3)
def make_bank_account():
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
frappe.get_doc(
{
"doctype": "Account",
"account_type": "Bank",
"account_name": "_Test Bank Clearance",
"company": "_Test Company",
"parent_account": "Bank Accounts - _TC",
}
).insert()
def create_loan_masters():
create_loan_type(
"Clearance Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"_Test Bank Clearance - _TC",
"_Test Bank Clearance - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
)
def add_transactions():
make_payment_entry()
make_loan()
def make_loan():
loan = create_loan(
"_Test Customer",
"Clearance Loan",
280000,
"Repay Over Number of Periods",
20,
applicant_type="Customer",
)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
repayment_entry.save()
repayment_entry.submit()
def make_payment_entry():
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()

@ -114,10 +114,13 @@ class OpeningInvoiceCreationTool(Document):
) )
or {} or {}
) )
default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency")
if company_details: if company_details:
invoice.update( invoice.update(
{ {
"currency": company_details.get("default_currency"), "currency": default_currency or company_details.get("default_currency"),
"letter_head": company_details.get("default_letter_head"), "letter_head": company_details.get("default_letter_head"),
} }
) )

@ -669,7 +669,7 @@ class PurchaseInvoice(BuyingController):
exchange_rate_map, net_rate_map = get_purchase_document_details(self) exchange_rate_map, net_rate_map = get_purchase_document_details(self)
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
) )
provisional_accounting_for_non_stock_items = cint( provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value( frappe.db.get_value(
@ -1159,7 +1159,7 @@ class PurchaseInvoice(BuyingController):
# tax table gl entries # tax table gl entries
valuation_tax = {} valuation_tax = {}
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
) )
for tax in self.get("taxes"): for tax in self.get("taxes"):
@ -1252,7 +1252,7 @@ class PurchaseInvoice(BuyingController):
def enable_discount_accounting(self): def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"): if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint( self._enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
) )
return self._enable_discount_accounting return self._enable_discount_accounting
@ -1369,7 +1369,9 @@ class PurchaseInvoice(BuyingController):
if ( if (
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
): ):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
)
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(

@ -5,6 +5,7 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import change_settings
from frappe.utils import add_days, cint, flt, getdate, nowdate, today from frappe.utils import add_days, cint, flt, getdate, nowdate, today
import erpnext import erpnext
@ -336,8 +337,8 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
@change_settings("Buying Settings", {"enable_discount_accounting": 1})
def test_purchase_invoice_with_discount_accounting_enabled(self): def test_purchase_invoice_with_discount_accounting_enabled(self):
enable_discount_accounting()
discount_account = create_account( discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
@ -353,10 +354,10 @@ class TestPurchaseInvoice(unittest.TestCase):
] ]
check_gl_entries(self, pi.name, expected_gle, nowdate()) check_gl_entries(self, pi.name, expected_gle, nowdate())
enable_discount_accounting(enable=0)
@change_settings("Buying Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self):
enable_discount_accounting()
additional_discount_account = create_account( additional_discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
parent_account="Indirect Expenses - _TC", parent_account="Indirect Expenses - _TC",
@ -1588,12 +1589,6 @@ def unlink_payment_on_cancel_of_invoice(enable=1):
accounts_settings.save() accounts_settings.save()
def enable_discount_accounting(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.enable_discount_accounting = enable
accounts_settings.save()
def make_purchase_invoice(**args): def make_purchase_invoice(**args):
pi = frappe.new_doc("Purchase Invoice") pi = frappe.new_doc("Purchase Invoice")
args = frappe._dict(args) args = frappe._dict(args)

@ -1051,7 +1051,7 @@ class SalesInvoice(SellingController):
def make_tax_gl_entries(self, gl_entries): def make_tax_gl_entries(self, gl_entries):
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
) )
for tax in self.get("taxes"): for tax in self.get("taxes"):
@ -1097,7 +1097,7 @@ class SalesInvoice(SellingController):
def make_item_gl_entries(self, gl_entries): def make_item_gl_entries(self, gl_entries):
# income account gl entries # income account gl entries
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
) )
for item in self.get("items"): for item in self.get("items"):
@ -1276,7 +1276,7 @@ class SalesInvoice(SellingController):
def enable_discount_accounting(self): def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"): if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint( self._enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
) )
return self._enable_discount_accounting return self._enable_discount_accounting
@ -1466,7 +1466,9 @@ class SalesInvoice(SellingController):
and self.base_rounding_adjustment and self.base_rounding_adjustment
and not self.is_internal_transfer() and not self.is_internal_transfer()
): ):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Sales Invoice", self.name
)
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(

@ -7,6 +7,7 @@ import unittest
import frappe import frappe
from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.tests.utils import change_settings
from frappe.utils import add_days, flt, getdate, nowdate from frappe.utils import add_days, flt, getdate, nowdate
import erpnext import erpnext
@ -1977,6 +1978,13 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][2], gle.credit) self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_rounding_adjustment_3(self): def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
create_dimension()
si = create_sales_invoice(do_not_save=True) si = create_sales_invoice(do_not_save=True)
si.items = [] si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
@ -2004,6 +2012,10 @@ class TestSalesInvoice(unittest.TestCase):
"included_in_print_rate": 1, "included_in_print_rate": 1,
}, },
) )
si.cost_center = "_Test Cost Center 2 - _TC"
si.location = "Block 1"
si.save() si.save()
si.submit() si.submit()
self.assertEqual(si.net_total, 4007.16) self.assertEqual(si.net_total, 4007.16)
@ -2039,6 +2051,18 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(debit_credit_diff, 0) self.assertEqual(debit_credit_diff, 0)
round_off_gle = frappe.db.get_value(
"GL Entry",
{"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"},
["cost_center", "location"],
as_dict=1,
)
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
self.assertEqual(round_off_gle.location, "Block 1")
disable_dimension()
def test_sales_invoice_with_shipping_rule(self): def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
@ -2684,12 +2708,8 @@ class TestSalesInvoice(unittest.TestCase):
sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC" sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC"
) )
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self): def test_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
enable_discount_accounting,
)
enable_discount_accounting()
discount_account = create_account( discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
@ -2705,14 +2725,10 @@ class TestSalesInvoice(unittest.TestCase):
] ]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0)
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
enable_discount_accounting,
)
enable_discount_accounting()
additional_discount_account = create_account( additional_discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
parent_account="Indirect Expenses - _TC", parent_account="Indirect Expenses - _TC",
@ -2743,7 +2759,6 @@ class TestSalesInvoice(unittest.TestCase):
] ]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0)
def test_asset_depreciation_on_sale_with_pro_rata(self): def test_asset_depreciation_on_sale_with_pro_rata(self):
""" """

@ -163,10 +163,15 @@ def get_party_details(party, party_type, args=None):
def get_tax_template(posting_date, args): def get_tax_template(posting_date, args):
"""Get matching tax rule""" """Get matching tax rule"""
args = frappe._dict(args) args = frappe._dict(args)
from_date = to_date = posting_date
if not posting_date:
from_date = "1900-01-01"
to_date = "4000-01-01"
conditions = [ conditions = [
"""(from_date is null or from_date <= '{0}') """(from_date is null or from_date <= '{0}')
and (to_date is null or to_date >= '{0}')""".format( and (to_date is null or to_date >= '{1}')""".format(
posting_date from_date, to_date
) )
] ]

@ -355,7 +355,7 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_
def make_round_off_gle(gl_map, debit_credit_diff, precision): def make_round_off_gle(gl_map, debit_credit_diff, precision):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
gl_map[0].company gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
) )
round_off_account_exists = False round_off_account_exists = False
round_off_gle = frappe._dict() round_off_gle = frappe._dict()
@ -392,14 +392,43 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
} }
) )
update_accounting_dimensions(round_off_gle)
if not round_off_account_exists: if not round_off_account_exists:
gl_map.append(round_off_gle) gl_map.append(round_off_gle)
def get_round_off_account_and_cost_center(company): def update_accounting_dimensions(round_off_gle):
dimensions = get_accounting_dimensions()
meta = frappe.get_meta(round_off_gle["voucher_type"])
has_all_dimensions = True
for dimension in dimensions:
if not meta.has_field(dimension):
has_all_dimensions = False
if dimensions and has_all_dimensions:
dimension_values = frappe.db.get_value(
round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1
)
for dimension in dimensions:
round_off_gle[dimension] = dimension_values.get(dimension)
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
round_off_account, round_off_cost_center = frappe.get_cached_value( round_off_account, round_off_cost_center = frappe.get_cached_value(
"Company", company, ["round_off_account", "round_off_cost_center"] "Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None] ) or [None, None]
meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE
if meta.has_field("cost_center"):
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
if parent_cost_center:
round_off_cost_center = parent_cost_center
if not round_off_account: if not round_off_account:
frappe.throw(_("Please mention Round Off Account in Company")) frappe.throw(_("Please mention Round Off Account in Company"))

@ -831,9 +831,9 @@ def get_party_shipping_address(doctype, name):
"where " "where "
"dl.link_doctype=%s " "dl.link_doctype=%s "
"and dl.link_name=%s " "and dl.link_name=%s "
'and dl.parenttype="Address" ' "and dl.parenttype='Address' "
"and ifnull(ta.disabled, 0) = 0 and" "and ifnull(ta.disabled, 0) = 0 and"
'(ta.address_type="Shipping" or ta.is_shipping_address=1) ' "(ta.address_type='Shipping' or ta.is_shipping_address=1) "
"order by ta.is_shipping_address desc, ta.address_type desc limit 1", "order by ta.is_shipping_address desc, ta.address_type desc limit 1",
(doctype, name), (doctype, name),
) )
@ -881,11 +881,11 @@ def get_default_contact(doctype, name):
""" """
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
FROM `tabDynamic Link` dl FROM `tabDynamic Link` dl
INNER JOIN tabContact c ON c.name = dl.parent INNER JOIN `tabContact` c ON c.name = dl.parent
WHERE WHERE
dl.link_doctype=%s AND dl.link_doctype=%s AND
dl.link_name=%s AND dl.link_name=%s AND
dl.parenttype = "Contact" dl.parenttype = 'Contact'
ORDER BY is_primary_contact DESC, is_billing_contact DESC ORDER BY is_primary_contact DESC, is_billing_contact DESC
""", """,
(doctype, name), (doctype, name),

@ -20,6 +20,7 @@
"maintain_same_rate", "maintain_same_rate",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice", "bill_for_rejected_quantity_in_purchase_invoice",
"enable_discount_accounting",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
@ -133,6 +134,13 @@
{ {
"fieldname": "column_break_12", "fieldname": "column_break_12",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting for Buying"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -140,7 +148,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-01-27 17:57:58.367048", "modified": "2022-04-14 15:56:42.340223",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

@ -5,10 +5,15 @@
import frappe import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint
class BuyingSettings(Document): class BuyingSettings(Document):
def on_update(self):
self.toggle_discount_accounting_fields()
def validate(self): def validate(self):
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
frappe.db.set_default(key, self.get(key, "")) frappe.db.set_default(key, self.get(key, ""))
@ -21,3 +26,60 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series", self.get("supp_master_name") == "Naming Series",
hide_name_field=False, hide_name_field=False,
) )
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)

@ -1079,9 +1079,14 @@ class AccountsController(TransactionBase):
return amount, base_amount return amount, base_amount
def make_discount_gl_entries(self, gl_entries): def make_discount_gl_entries(self, gl_entries):
enable_discount_accounting = cint( if self.doctype == "Purchase Invoice":
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") enable_discount_accounting = cint(
) frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
elif self.doctype == "Sales Invoice":
enable_discount_accounting = cint(
frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
)
if enable_discount_accounting: if enable_discount_accounting:
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":

@ -54,11 +54,11 @@ class Opportunity(TransactionBase):
self.calculate_totals() self.calculate_totals()
def map_fields(self): def map_fields(self):
for field in self.meta.fields: for field in self.meta.get_valid_columns():
if not self.get(field.fieldname): if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
try: try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname) value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
frappe.db.set(self, field.fieldname, value) frappe.db.set(self, field, value)
except Exception: except Exception:
continue continue

@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, get_datetime from frappe.utils import cint, get_datetime, get_link_to_form
from erpnext.hr.doctype.attendance.attendance import ( from erpnext.hr.doctype.attendance.attendance import (
get_duplicate_attendance_record, get_duplicate_attendance_record,
@ -130,14 +130,11 @@ def mark_attendance_and_link_log(
""" """
log_names = [x.name for x in logs] log_names = [x.name for x in logs]
employee = logs[0].employee employee = logs[0].employee
if attendance_status == "Skip": if attendance_status == "Skip":
frappe.db.sql( skip_attendance_in_checkins(log_names)
"""update `tabEmployee Checkin`
set skip_auto_attendance = %s
where name in %s""",
("1", log_names),
)
return None return None
elif attendance_status in ("Present", "Absent", "Half Day"): elif attendance_status in ("Present", "Absent", "Half Day"):
employee_doc = frappe.get_doc("Employee", employee) employee_doc = frappe.get_doc("Employee", employee)
duplicate = get_duplicate_attendance_record(employee, attendance_date, shift) duplicate = get_duplicate_attendance_record(employee, attendance_date, shift)
@ -159,6 +156,12 @@ def mark_attendance_and_link_log(
} }
attendance = frappe.get_doc(doc_dict).insert() attendance = frappe.get_doc(doc_dict).insert()
attendance.submit() attendance.submit()
if attendance_status == "Absent":
attendance.add_comment(
text=_("Employee was marked Absent for not meeting the working hours threshold.")
)
frappe.db.sql( frappe.db.sql(
"""update `tabEmployee Checkin` """update `tabEmployee Checkin`
set attendance = %s set attendance = %s
@ -167,13 +170,10 @@ def mark_attendance_and_link_log(
) )
return attendance return attendance
else: else:
frappe.db.sql( skip_attendance_in_checkins(log_names)
"""update `tabEmployee Checkin` add_comment_in_checkins(log_names, duplicate, overlapping)
set skip_auto_attendance = %s
where name in %s""",
("1", log_names),
)
return None return None
else: else:
frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status)) frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status))
@ -241,3 +241,34 @@ def time_diff_in_hours(start, end):
def find_index_in_dict(dict_list, key, value): def find_index_in_dict(dict_list, key, value):
return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None) return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None)
def add_comment_in_checkins(log_names, duplicate, overlapping):
if duplicate:
text = _("Auto Attendance skipped due to duplicate attendance record: {}").format(
get_link_to_form("Attendance", duplicate[0].name)
)
else:
text = _("Auto Attendance skipped due to overlapping attendance record: {}").format(
get_link_to_form("Attendance", overlapping.name)
)
for name in log_names:
frappe.get_doc(
{
"doctype": "Comment",
"comment_type": "Comment",
"reference_doctype": "Employee Checkin",
"reference_name": name,
"content": text,
}
).insert(ignore_permissions=True)
def skip_attendance_in_checkins(log_names):
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
(
frappe.qb.update(EmployeeCheckin)
.set("skip_auto_attendance", 1)
.where(EmployeeCheckin.name.isin(log_names))
).run()

@ -18,6 +18,7 @@ class TestLeaveAllocation(FrappeTestCase):
def setUp(self): def setUp(self):
frappe.db.delete("Leave Period") frappe.db.delete("Leave Period")
frappe.db.delete("Leave Allocation") frappe.db.delete("Leave Allocation")
frappe.db.delete("Leave Ledger Entry")
emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company") emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
self.employee = frappe.get_doc("Employee", emp_id) self.employee = frappe.get_doc("Employee", emp_id)

@ -134,7 +134,17 @@ class ShiftType(Document):
shift_details = get_employee_shift(employee, timestamp, True) shift_details = get_employee_shift(employee, timestamp, True)
if shift_details and shift_details.shift_type.name == self.name: if shift_details and shift_details.shift_type.name == self.name:
mark_attendance(employee, date, "Absent", self.name) attendance = mark_attendance(employee, date, "Absent", self.name)
if attendance:
frappe.get_doc(
{
"doctype": "Comment",
"comment_type": "Comment",
"reference_doctype": "Attendance",
"reference_name": attendance,
"content": frappe._("Employee was marked Absent due to missing Employee Checkins."),
}
).insert(ignore_permissions=True)
def get_start_and_end_dates(self, employee): def get_start_and_end_dates(self, employee):
"""Returns start and end dates for checking attendance and marking absent """Returns start and end dates for checking attendance and marking absent

@ -753,7 +753,7 @@ class BOM(WebsiteGenerator):
bom_item.include_item_in_manufacturing, bom_item.include_item_in_manufacturing,
bom_item.sourced_by_supplier, bom_item.sourced_by_supplier,
bom_item.stock_qty / ifnull(bom.quantity, 1) AS qty_consumed_per_unit bom_item.stock_qty / ifnull(bom.quantity, 1) AS qty_consumed_per_unit
FROM `tabBOM Explosion Item` bom_item, tabBOM bom FROM `tabBOM Explosion Item` bom_item, `tabBOM` bom
WHERE WHERE
bom_item.parent = bom.name bom_item.parent = bom.name
AND bom.name = %s AND bom.name = %s

@ -365,4 +365,5 @@ erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
erpnext.patches.v14_0.discount_accounting_separation
erpnext.patches.v14_0.delete_employee_transfer_property_doctype erpnext.patches.v14_0.delete_employee_transfer_property_doctype

@ -0,0 +1,9 @@
import frappe
def execute():
doc = frappe.get_doc("Accounts Settings")
discount_account = doc.enable_discount_accounting
if discount_account:
for doctype in ["Buying Settings", "Selling Settings"]:
frappe.db.set_value(doctype, doctype, "enable_discount_accounting", 1, update_modified=False)

@ -553,6 +553,7 @@ def validate_totals(einvoice):
+ flt(value_details["CgstVal"]) + flt(value_details["CgstVal"])
+ flt(value_details["SgstVal"]) + flt(value_details["SgstVal"])
+ flt(value_details["IgstVal"]) + flt(value_details["IgstVal"])
+ flt(value_details["CesVal"])
+ flt(value_details["OthChrg"]) + flt(value_details["OthChrg"])
+ flt(value_details["RndOffAmt"]) + flt(value_details["RndOffAmt"])
- flt(value_details["Discount"]) - flt(value_details["Discount"])

@ -1219,7 +1219,7 @@ def make_fixtures(company=None):
try: try:
doc = frappe.get_doc(d) doc = frappe.get_doc(d)
doc.flags.ignore_permissions = True doc.flags.ignore_permissions = True
doc.insert() doc.insert(ignore_if_duplicate=True)
except frappe.NameError: except frappe.NameError:
frappe.clear_messages() frappe.clear_messages()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:

@ -152,7 +152,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} }
} }
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) {
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
}
const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1; const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1;
const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1; const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1;

@ -1520,6 +1520,7 @@
"fieldname": "per_picked", "fieldname": "per_picked",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "% Picked", "label": "% Picked",
"no_copy": 1,
"read_only": 1 "read_only": 1
} }
], ],
@ -1527,7 +1528,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-03-15 21:38:31.437586", "modified": "2022-04-21 08:16:48.316074",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

@ -385,6 +385,16 @@ class SalesOrder(SellingController):
if tot_qty != 0: if tot_qty != 0:
self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False) self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False)
def update_picking_status(self):
total_picked_qty = 0.0
total_qty = 0.0
for so_item in self.items:
total_picked_qty += flt(so_item.picked_qty)
total_qty += flt(so_item.stock_qty)
per_picked = total_picked_qty / total_qty * 100
self.db_set("per_picked", flt(per_picked), update_modified=False)
def set_indicator(self): def set_indicator(self):
"""Set indicator for portal""" """Set indicator for portal"""
if self.per_billed < 100 and self.per_delivered < 100: if self.per_billed < 100 and self.per_delivered < 100:
@ -1232,9 +1242,30 @@ def make_inter_company_purchase_order(source_name, target_doc=None):
@frappe.whitelist() @frappe.whitelist()
def create_pick_list(source_name, target_doc=None): def create_pick_list(source_name, target_doc=None):
def update_item_quantity(source, target, source_parent): from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle
target.qty = flt(source.qty) - flt(source.delivered_qty)
target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor) def update_item_quantity(source, target, source_parent) -> None:
picked_qty = flt(source.picked_qty) / (flt(source.conversion_factor) or 1)
qty_to_be_picked = flt(source.qty) - max(picked_qty, flt(source.delivered_qty))
target.qty = qty_to_be_picked
target.stock_qty = qty_to_be_picked * flt(source.conversion_factor)
def update_packed_item_qty(source, target, source_parent) -> None:
qty = flt(source.qty)
for item in source_parent.items:
if source.parent_detail_docname == item.name:
picked_qty = flt(item.picked_qty) / (flt(item.conversion_factor) or 1)
pending_percent = (item.qty - max(picked_qty, item.delivered_qty)) / item.qty
target.qty = target.stock_qty = qty * pending_percent
return
def should_pick_order_item(item) -> bool:
return (
abs(item.delivered_qty) < abs(item.qty)
and item.delivered_by_supplier != 1
and not is_product_bundle(item.item_code)
)
doc = get_mapped_doc( doc = get_mapped_doc(
"Sales Order", "Sales Order",
@ -1245,8 +1276,17 @@ def create_pick_list(source_name, target_doc=None):
"doctype": "Pick List Item", "doctype": "Pick List Item",
"field_map": {"parent": "sales_order", "name": "sales_order_item"}, "field_map": {"parent": "sales_order", "name": "sales_order_item"},
"postprocess": update_item_quantity, "postprocess": update_item_quantity,
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) "condition": should_pick_order_item,
and doc.delivered_by_supplier != 1, },
"Packed Item": {
"doctype": "Pick List Item",
"field_map": {
"parent": "sales_order",
"name": "sales_order_item",
"parent_detail_docname": "product_bundle_item",
},
"field_no_map": ["picked_qty"],
"postprocess": update_packed_item_qty,
}, },
}, },
target_doc, target_doc,

@ -803,13 +803,15 @@
{ {
"fieldname": "picked_qty", "fieldname": "picked_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Picked Qty" "label": "Picked Qty (in Stock UOM)",
"no_copy": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-03-15 20:17:33.984799", "modified": "2022-04-27 03:15:34.366563",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

@ -27,7 +27,8 @@
"column_break_5", "column_break_5",
"allow_multiple_items", "allow_multiple_items",
"allow_against_multiple_purchase_orders", "allow_against_multiple_purchase_orders",
"hide_tax_id" "hide_tax_id",
"enable_discount_accounting"
], ],
"fields": [ "fields": [
{ {
@ -164,6 +165,13 @@
"fieldname": "editable_bundle_item_rates", "fieldname": "editable_bundle_item_rates",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Calculate Product Bundle Price based on Child Items' Rates" "label": "Calculate Product Bundle Price based on Child Items' Rates"
},
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting for Selling"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -171,7 +179,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-02-04 15:41:59.939261", "modified": "2022-04-14 16:01:29.405642",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",

@ -14,6 +14,7 @@ class SellingSettings(Document):
def on_update(self): def on_update(self):
self.toggle_hide_tax_id() self.toggle_hide_tax_id()
self.toggle_editable_rate_for_bundle_items() self.toggle_editable_rate_for_bundle_items()
self.toggle_discount_accounting_fields()
def validate(self): def validate(self):
for key in [ for key in [
@ -58,3 +59,60 @@ class SellingSettings(Document):
"Check", "Check",
validate_fields_for_doctype=False, validate_fields_for_doctype=False,
) )
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
make_property_setter(
"Sales Invoice Item",
"discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Sales Invoice Item",
"discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Sales Invoice Item",
"discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
make_property_setter(
"Sales Invoice",
"additional_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Sales Invoice",
"additional_discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Sales Invoice",
"additional_discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)

@ -343,9 +343,9 @@ erpnext.PointOfSale.Controller = class {
toggle_other_sections: (show) => { toggle_other_sections: (show) => {
if (show) { if (show) {
this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : ''; this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : '';
this.item_selector.$component.css('display', 'none'); this.item_selector.toggle_component(false);
} else { } else {
this.item_selector.$component.css('display', 'flex'); this.item_selector.toggle_component(true);
} }
}, },

@ -130,10 +130,10 @@ erpnext.PointOfSale.ItemCart = class {
}, },
cols: 5, cols: 5,
keys: [ keys: [
[ 1, 2, 3, __('Quantity') ], [ 1, 2, 3, 'Quantity' ],
[ 4, 5, 6, __('Discount') ], [ 4, 5, 6, 'Discount' ],
[ 7, 8, 9, __('Rate') ], [ 7, 8, 9, 'Rate' ],
[ '.', 0, __('Delete'), __('Remove') ] [ '.', 0, 'Delete', 'Remove' ]
], ],
css_classes: [ css_classes: [
[ '', '', '', 'col-span-2' ], [ '', '', '', 'col-span-2' ],

@ -179,6 +179,25 @@ erpnext.PointOfSale.ItemSelector = class {
}); });
this.search_field.toggle_label(false); this.search_field.toggle_label(false);
this.item_group_field.toggle_label(false); this.item_group_field.toggle_label(false);
this.attach_clear_btn();
}
attach_clear_btn() {
this.search_field.$wrapper.find('.control-input').append(
`<span class="link-btn" style="top: 2px;">
<a class="btn-open no-decoration" title="${__("Clear")}">
${frappe.utils.icon('close', 'sm')}
</a>
</span>`
);
this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn');
this.$clear_search_btn.on('click', 'a', () => {
this.set_search_value('');
this.search_field.set_focus();
});
} }
set_search_value(value) { set_search_value(value) {
@ -252,6 +271,16 @@ erpnext.PointOfSale.ItemSelector = class {
const search_term = e.target.value; const search_term = e.target.value;
this.filter_items({ search_term }); this.filter_items({ search_term });
}, 300); }, 300);
this.$clear_search_btn.toggle(
Boolean(this.search_field.$input.val())
);
});
this.search_field.$input.on('focus', () => {
this.$clear_search_btn.toggle(
Boolean(this.search_field.$input.val())
);
}); });
} }
@ -284,7 +313,7 @@ erpnext.PointOfSale.ItemSelector = class {
if (this.items.length == 1) { if (this.items.length == 1) {
this.$items_container.find(".item-wrapper").click(); this.$items_container.find(".item-wrapper").click();
frappe.utils.play_sound("submit"); frappe.utils.play_sound("submit");
$(this.search_field.$input[0]).val("").trigger("input"); this.set_search_value('');
} else if (this.items.length == 0 && this.barcode_scanned) { } else if (this.items.length == 0 && this.barcode_scanned) {
// only show alert of barcode is scanned and enter is pressed // only show alert of barcode is scanned and enter is pressed
frappe.show_alert({ frappe.show_alert({
@ -293,7 +322,7 @@ erpnext.PointOfSale.ItemSelector = class {
}); });
frappe.utils.play_sound("error"); frappe.utils.play_sound("error");
this.barcode_scanned = false; this.barcode_scanned = false;
$(this.search_field.$input[0]).val("").trigger("input"); this.set_search_value('');
} }
}); });
} }
@ -350,6 +379,7 @@ erpnext.PointOfSale.ItemSelector = class {
} }
toggle_component(show) { toggle_component(show) {
show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); this.set_search_value('');
this.$component.css('display', show ? 'flex': 'none');
} }
}; };

@ -25,7 +25,7 @@ erpnext.PointOfSale.NumberPad = class {
const fieldname = fieldnames && fieldnames[number] ? const fieldname = fieldnames && fieldnames[number] ?
fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number;
return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${number}</div>`; return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${__(number)}</div>`;
}, ''); }, '');
}, ''); }, '');
} }

@ -5,7 +5,7 @@
"label": "Sales Order Trends" "label": "Sales Order Trends"
} }
], ],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"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\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"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\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-01-28 11:49:12.092882", "creation": "2020-01-28 11:49:12.092882",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
@ -314,118 +314,11 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Analytics",
"link_count": 0,
"link_to": "Sales Analytics",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Order Analysis",
"link_count": 0,
"link_to": "Sales Order Analysis",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Sales Funnel",
"link_count": 0,
"link_to": "sales-funnel",
"link_type": "Page",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Order Trends",
"link_count": 0,
"link_to": "Sales Order Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Quotation",
"hidden": 0,
"is_query_report": 1,
"label": "Quotation Trends",
"link_count": 0,
"link_to": "Quotation Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Customer Acquisition and Loyalty",
"link_count": 0,
"link_to": "Customer Acquisition and Loyalty",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Inactive Customers",
"link_count": 0,
"link_to": "Inactive Customers",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Person-wise Transaction Summary",
"link_count": 0,
"link_to": "Sales Person-wise Transaction Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Item",
"hidden": 0,
"is_query_report": 1,
"label": "Item-wise Sales History",
"link_count": 0,
"link_to": "Item-wise Sales History",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Other Reports", "label": "Other Reports",
"link_count": 0, "link_count": 12,
"onboard": 0, "onboard": 0,
"type": "Card Break" "type": "Card Break"
}, },
@ -560,9 +453,258 @@
"link_type": "Report", "link_type": "Report",
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 22,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Analytics",
"link_count": 0,
"link_to": "Sales Analytics",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Order Analysis",
"link_count": 0,
"link_to": "Sales Order Analysis",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Sales Funnel",
"link_count": 0,
"link_to": "sales-funnel",
"link_type": "Page",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Order Trends",
"link_count": 0,
"link_to": "Sales Order Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Quotation",
"hidden": 0,
"is_query_report": 1,
"label": "Quotation Trends",
"link_count": 0,
"link_to": "Quotation Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Customer Acquisition and Loyalty",
"link_count": 0,
"link_to": "Customer Acquisition and Loyalty",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Inactive Customers",
"link_count": 0,
"link_to": "Inactive Customers",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Person-wise Transaction Summary",
"link_count": 0,
"link_to": "Sales Person-wise Transaction Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Item",
"hidden": 0,
"is_query_report": 1,
"label": "Item-wise Sales History",
"link_count": 0,
"link_to": "Item-wise Sales History",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Lead",
"hidden": 0,
"is_query_report": 1,
"label": "Lead Details",
"link_count": 0,
"link_to": "Lead Details",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Address",
"hidden": 0,
"is_query_report": 1,
"label": "Customer Addresses And Contacts",
"link_count": 0,
"link_to": "Address And Contacts",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Item",
"hidden": 0,
"is_query_report": 1,
"label": "Available Stock for Packing Items",
"link_count": 0,
"link_to": "Available Stock for Packing Items",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Pending SO Items For Purchase Request",
"link_count": 0,
"link_to": "Pending SO Items For Purchase Request",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Delivery Note",
"hidden": 0,
"is_query_report": 1,
"label": "Delivery Note Trends",
"link_count": 0,
"link_to": "Delivery Note Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Invoice Trends",
"link_count": 0,
"link_to": "Sales Invoice Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Customer Credit Balance",
"link_count": 0,
"link_to": "Customer Credit Balance",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Customers Without Any Sales Transactions",
"link_count": 0,
"link_to": "Customers Without Any Sales Transactions",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Partners Commission",
"link_count": 0,
"link_to": "Sales Partners Commission",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Territory Target Variance Based On Item Group",
"link_count": 0,
"link_to": "Territory Target Variance Based On Item Group",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Person Target Variance Based On Item Group",
"link_count": 0,
"link_to": "Sales Person Target Variance Based On Item Group",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Partner Target Variance Based On Item Group",
"link_count": 0,
"link_to": "Sales Partner Target Variance based on Item Group",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Payment Terms Status for Sales Order",
"link_count": 0,
"link_to": "Payment Terms Status for Sales Order",
"link_type": "Report",
"onboard": 0,
"type": "Link"
} }
], ],
"modified": "2022-01-13 17:43:02.778627", "modified": "2022-04-26 13:29:55.087240",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling", "name": "Selling",

@ -78,56 +78,6 @@ class TestDeliveryNote(FrappeTestCase):
self.assertFalse(get_gl_entries("Delivery Note", dn.name)) self.assertFalse(get_gl_entries("Delivery Note", dn.name))
# def test_delivery_note_gl_entry(self):
# company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
# set_valuation_method("_Test Item", "FIFO")
# make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
# stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory')
# prev_bal = get_balance_on(stock_in_hand_account)
# dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
# gl_entries = get_gl_entries("Delivery Note", dn.name)
# self.assertTrue(gl_entries)
# stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry",
# {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference"))
# expected_values = {
# stock_in_hand_account: [0.0, stock_value_difference],
# "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0]
# }
# for i, gle in enumerate(gl_entries):
# self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
# # check stock in hand balance
# bal = get_balance_on(stock_in_hand_account)
# self.assertEqual(bal, prev_bal - stock_value_difference)
# # back dated incoming entry
# make_stock_entry(posting_date=add_days(nowdate(), -2), target="Stores - TCP1",
# qty=5, basic_rate=100)
# gl_entries = get_gl_entries("Delivery Note", dn.name)
# self.assertTrue(gl_entries)
# stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry",
# {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference"))
# expected_values = {
# stock_in_hand_account: [0.0, stock_value_difference],
# "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0]
# }
# for i, gle in enumerate(gl_entries):
# self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
# dn.cancel()
# self.assertTrue(get_gl_entries("Delivery Note", dn.name))
# set_perpetual_inventory(0, company)
def test_delivery_note_gl_entry_packing_item(self): def test_delivery_note_gl_entry_packing_item(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@ -854,8 +804,6 @@ class TestDeliveryNote(FrappeTestCase):
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
) )
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
set_valuation_method("_Test Item", "FIFO") set_valuation_method("_Test Item", "FIFO")
make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
@ -881,8 +829,6 @@ class TestDeliveryNote(FrappeTestCase):
def test_delivery_note_cost_center_with_balance_sheet_account(self): def test_delivery_note_cost_center_with_balance_sheet_account(self):
cost_center = "Main - TCP1" cost_center = "Main - TCP1"
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
set_valuation_method("_Test Item", "FIFO") set_valuation_method("_Test Item", "FIFO")
make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)

@ -5,6 +5,8 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Cast_
from frappe.utils import getdate from frappe.utils import getdate
@ -48,35 +50,57 @@ class ItemPrice(Document):
) )
def check_duplicates(self): def check_duplicates(self):
conditions = (
"""where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s"""
)
for field in [ item_price = frappe.qb.DocType("Item Price")
query = (
frappe.qb.from_(item_price)
.select(item_price.price_list_rate)
.where(
(item_price.item_code == self.item_code)
& (item_price.price_list == self.price_list)
& (item_price.name != self.name)
)
)
data_fields = (
"uom", "uom",
"valid_from", "valid_from",
"valid_upto", "valid_upto",
"packing_unit",
"customer", "customer",
"supplier", "supplier",
"batch_no", "batch_no",
]:
if self.get(field):
conditions += " and {0} = %({0})s ".format(field)
else:
conditions += "and (isnull({0}) or {0} = '')".format(field)
price_list_rate = frappe.db.sql(
"""
select price_list_rate
from `tabItem Price`
{conditions}
""".format(
conditions=conditions
),
self.as_dict(),
) )
number_fields = ["packing_unit"]
for field in data_fields:
if self.get(field):
query = query.where(item_price[field] == self.get(field))
else:
query = query.where(
Criterion.any(
[
item_price[field].isnull(),
Cast_(item_price[field], "varchar") == "",
]
)
)
for field in number_fields:
if self.get(field):
query = query.where(item_price[field] == self.get(field))
else:
query = query.where(
Criterion.any(
[
item_price[field].isnull(),
item_price[field] == 0,
]
)
)
price_list_rate = query.run(as_dict=True)
if price_list_rate: if price_list_rate:
frappe.throw( frappe.throw(
_( _(

@ -29,6 +29,7 @@
"ordered_qty", "ordered_qty",
"column_break_16", "column_break_16",
"incoming_rate", "incoming_rate",
"picked_qty",
"page_break", "page_break",
"prevdoc_doctype", "prevdoc_doctype",
"parent_detail_docname" "parent_detail_docname"
@ -234,13 +235,20 @@
"label": "Ordered Qty", "label": "Ordered Qty",
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "picked_qty",
"fieldtype": "Float",
"label": "Picked Qty",
"no_copy": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-03-10 15:42:00.265915", "modified": "2022-04-27 05:23:08.683245",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Packed Item", "name": "Packed Item",

@ -32,7 +32,7 @@ def make_packing_list(doc):
reset = reset_packing_list(doc) reset = reset_packing_list(doc)
for item_row in doc.get("items"): for item_row in doc.get("items"):
if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): if is_product_bundle(item_row.item_code):
for bundle_item in get_product_bundle_items(item_row.item_code): for bundle_item in get_product_bundle_items(item_row.item_code):
pi_row = add_packed_item_row( pi_row = add_packed_item_row(
doc=doc, doc=doc,
@ -54,6 +54,10 @@ def make_packing_list(doc):
set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item
def is_product_bundle(item_code: str) -> bool:
return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code}))
def get_indexed_packed_items_table(doc): def get_indexed_packed_items_table(doc):
""" """
Create dict from stale packed items table like: Create dict from stale packed items table like:

@ -1,10 +1,12 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from typing import List, Optional, Tuple
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_to_date, nowdate from frappe.utils import add_to_date, nowdate
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
@ -12,6 +14,33 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
def create_product_bundle(
quantities: Optional[List[int]] = None, warehouse: Optional[str] = None
) -> Tuple[str, List[str]]:
"""Get a new product_bundle for use in tests.
Create 10x required stock if warehouse is specified.
"""
if not quantities:
quantities = [2, 2]
bundle = make_item(properties={"is_stock_item": 0}).name
bundle_doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": bundle})
components = []
for qty in quantities:
compoenent = make_item().name
components.append(compoenent)
bundle_doc.append("items", {"item_code": compoenent, "qty": qty})
if warehouse:
make_stock_entry(item=compoenent, to_warehouse=warehouse, qty=10 * qty, rate=100)
bundle_doc.insert()
return bundle, components
class TestPackedItem(FrappeTestCase): class TestPackedItem(FrappeTestCase):
"Test impact on Packed Items table in various scenarios." "Test impact on Packed Items table in various scenarios."
@ -19,24 +48,11 @@ class TestPackedItem(FrappeTestCase):
def setUpClass(cls) -> None: def setUpClass(cls) -> None:
super().setUpClass() super().setUpClass()
cls.warehouse = "_Test Warehouse - _TC" cls.warehouse = "_Test Warehouse - _TC"
cls.bundle = "_Test Product Bundle X"
cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
cls.bundle2 = "_Test Product Bundle Y" cls.bundle, cls.bundle_items = create_product_bundle(warehouse=cls.warehouse)
cls.bundle2_items = ["_Test Bundle Item 3", "_Test Bundle Item 4"] cls.bundle2, cls.bundle2_items = create_product_bundle(warehouse=cls.warehouse)
make_item(cls.bundle, {"is_stock_item": 0}) cls.normal_item = make_item().name
make_item(cls.bundle2, {"is_stock_item": 0})
for item in cls.bundle_items + cls.bundle2_items:
make_item(item, {"is_stock_item": 1})
make_item("_Test Normal Stock Item", {"is_stock_item": 1})
make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
make_product_bundle(cls.bundle2, cls.bundle2_items, qty=2)
for item in cls.bundle_items + cls.bundle2_items:
make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100)
def test_adding_bundle_item(self): def test_adding_bundle_item(self):
"Test impact on packed items if bundle item row is added." "Test impact on packed items if bundle item row is added."
@ -58,7 +74,7 @@ class TestPackedItem(FrappeTestCase):
self.assertEqual(so.packed_items[1].qty, 4) self.assertEqual(so.packed_items[1].qty, 4)
# change item code to non bundle item # change item code to non bundle item
so.items[0].item_code = "_Test Normal Stock Item" so.items[0].item_code = self.normal_item
so.save() so.save()
self.assertEqual(len(so.packed_items), 0) self.assertEqual(len(so.packed_items), 0)

@ -114,6 +114,7 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"collapsible": 1,
"fieldname": "print_settings_section", "fieldname": "print_settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Print Settings" "label": "Print Settings"
@ -129,7 +130,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-10-05 15:08:40.369957", "modified": "2022-04-21 07:56:40.646473",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List", "name": "Pick List",
@ -199,5 +200,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

@ -4,13 +4,14 @@
import json import json
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from itertools import groupby from itertools import groupby
from operator import itemgetter from typing import Dict, List, Set
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import map_child_doc from frappe.model.mapper import map_child_doc
from frappe.utils import cint, floor, flt, today from frappe.utils import cint, floor, flt, today
from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import ( from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note as create_delivery_note_from_sales_order, make_delivery_note as create_delivery_note_from_sales_order,
@ -38,6 +39,7 @@ class PickList(Document):
) )
def before_submit(self): def before_submit(self):
update_sales_orders = set()
for item in self.locations: for item in self.locations:
# if the user has not entered any picked qty, set it to stock_qty, before submit # if the user has not entered any picked qty, set it to stock_qty, before submit
if item.picked_qty == 0: if item.picked_qty == 0:
@ -45,7 +47,8 @@ class PickList(Document):
if item.sales_order_item: if item.sales_order_item:
# update the picked_qty in SO Item # update the picked_qty in SO Item
self.update_so(item.sales_order_item, item.picked_qty, item.item_code) self.update_sales_order_item(item, item.picked_qty, item.item_code)
update_sales_orders.add(item.sales_order)
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
continue continue
@ -65,18 +68,29 @@ class PickList(Document):
title=_("Quantity Mismatch"), title=_("Quantity Mismatch"),
) )
self.update_bundle_picked_qty()
self.update_sales_order_picking_status(update_sales_orders)
def before_cancel(self): def before_cancel(self):
# update picked_qty in SO Item on cancel of PL """Deduct picked qty on cancelling pick list"""
updated_sales_orders = set()
for item in self.get("locations"): for item in self.get("locations"):
if item.sales_order_item: if item.sales_order_item:
self.update_so(item.sales_order_item, -1 * item.picked_qty, item.item_code) self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
updated_sales_orders.add(item.sales_order)
self.update_bundle_picked_qty()
self.update_sales_order_picking_status(updated_sales_orders)
def update_sales_order_item(self, item, picked_qty, item_code):
item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item"
stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty"
def update_so(self, so_item, picked_qty, item_code):
so_doc = frappe.get_doc(
"Sales Order", frappe.db.get_value("Sales Order Item", so_item, "parent")
)
already_picked, actual_qty = frappe.db.get_value( already_picked, actual_qty = frappe.db.get_value(
"Sales Order Item", so_item, ["picked_qty", "qty"] item_table,
item.sales_order_item,
["picked_qty", stock_qty_field],
) )
if self.docstatus == 1: if self.docstatus == 1:
@ -86,20 +100,16 @@ class PickList(Document):
frappe.throw( frappe.throw(
_( _(
"You are picking more than required quantity for {}. Check if there is any other pick list created for {}" "You are picking more than required quantity for {}. Check if there is any other pick list created for {}"
).format(item_code, so_doc.name) ).format(item_code, item.sales_order)
) )
frappe.db.set_value("Sales Order Item", so_item, "picked_qty", already_picked + picked_qty) frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty)
total_picked_qty = 0 @staticmethod
total_so_qty = 0 def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
for item in so_doc.get("items"): for sales_order in sales_orders:
total_picked_qty += flt(item.picked_qty) if sales_order:
total_so_qty += flt(item.stock_qty) frappe.get_doc("Sales Order", sales_order).update_picking_status()
total_picked_qty = total_picked_qty + picked_qty
per_picked = total_picked_qty / total_so_qty * 100
so_doc.db_set("per_picked", flt(per_picked), update_modified=False)
@frappe.whitelist() @frappe.whitelist()
def set_item_locations(self, save=False): def set_item_locations(self, save=False):
@ -109,7 +119,7 @@ class PickList(Document):
from_warehouses = None from_warehouses = None
if self.parent_warehouse: if self.parent_warehouse:
from_warehouses = frappe.db.get_descendants("Warehouse", self.parent_warehouse) from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
# Create replica before resetting, to handle empty table on update after submit. # Create replica before resetting, to handle empty table on update after submit.
locations_replica = self.get("locations") locations_replica = self.get("locations")
@ -190,8 +200,7 @@ class PickList(Document):
frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
def before_print(self, settings=None): def before_print(self, settings=None):
if self.get("group_same_items"): self.group_similar_items()
self.group_similar_items()
def group_similar_items(self): def group_similar_items(self):
group_item_qty = defaultdict(float) group_item_qty = defaultdict(float)
@ -217,6 +226,57 @@ class PickList(Document):
for idx, item in enumerate(self.locations, start=1): for idx, item in enumerate(self.locations, start=1):
item.idx = idx item.idx = idx
def update_bundle_picked_qty(self):
product_bundles = self._get_product_bundles()
product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values())
for so_row, item_code in product_bundles.items():
picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code])
item_table = "Sales Order Item"
already_picked = frappe.db.get_value(item_table, so_row, "picked_qty")
frappe.db.set_value(
item_table,
so_row,
"picked_qty",
already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
)
def _get_product_bundles(self) -> Dict[str, str]:
# Dict[so_item_row: item_code]
product_bundles = {}
for item in self.locations:
if not item.product_bundle_item:
continue
product_bundles[item.product_bundle_item] = frappe.db.get_value(
"Sales Order Item",
item.product_bundle_item,
"item_code",
)
return product_bundles
def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]:
# bundle_item_code: Dict[component, qty]
product_bundle_qty_map = {}
for bundle_item_code in bundles:
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code})
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
return product_bundle_qty_map
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
"""Compute how many full bundles can be created from picked items."""
precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
possible_bundles = []
for item in self.locations:
if item.product_bundle_item != bundle_row:
continue
if qty_in_bundle := bundle_items.get(item.item_code):
possible_bundles.append(item.picked_qty / qty_in_bundle)
else:
possible_bundles.append(0)
return int(flt(min(possible_bundles), precision or 6))
def validate_item_locations(pick_list): def validate_item_locations(pick_list):
if not pick_list.locations: if not pick_list.locations:
@ -450,22 +510,18 @@ def create_delivery_note(source_name, target_doc=None):
for location in pick_list.locations: for location in pick_list.locations:
if location.sales_order: if location.sales_order:
sales_orders.append( sales_orders.append(
[frappe.db.get_value("Sales Order", location.sales_order, "customer"), location.sales_order] frappe.db.get_value(
"Sales Order", location.sales_order, ["customer", "name as sales_order"], as_dict=True
)
) )
# Group sales orders by customer
for key, keydata in groupby(sales_orders, key=itemgetter(0)): for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]):
sales_dict[key] = set([d[1] for d in keydata]) sales_dict[customer] = {row.sales_order for row in rows}
if sales_dict: if sales_dict:
delivery_note = create_dn_with_so(sales_dict, pick_list) delivery_note = create_dn_with_so(sales_dict, pick_list)
is_item_wo_so = 0 if not all(item.sales_order for item in pick_list.locations):
for location in pick_list.locations:
if not location.sales_order:
is_item_wo_so = 1
break
if is_item_wo_so == 1:
# Create a DN for items without sales orders as well
delivery_note = create_dn_wo_so(pick_list) delivery_note = create_dn_wo_so(pick_list)
frappe.msgprint(_("Delivery Note(s) created for the Pick List")) frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
@ -492,27 +548,30 @@ def create_dn_wo_so(pick_list):
def create_dn_with_so(sales_dict, pick_list): def create_dn_with_so(sales_dict, pick_list):
delivery_note = None delivery_note = None
item_table_mapper = {
"doctype": "Delivery Note Item",
"field_map": {
"rate": "rate",
"name": "so_detail",
"parent": "against_sales_order",
},
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
and doc.delivered_by_supplier != 1,
}
for customer in sales_dict: for customer in sales_dict:
for so in sales_dict[customer]: for so in sales_dict[customer]:
delivery_note = None delivery_note = None
delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True) delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True)
item_table_mapper = {
"doctype": "Delivery Note Item",
"field_map": {
"rate": "rate",
"name": "so_detail",
"parent": "against_sales_order",
},
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
and doc.delivered_by_supplier != 1,
}
break break
if delivery_note: if delivery_note:
# map all items of all sales orders of that customer # map all items of all sales orders of that customer
for so in sales_dict[customer]: for so in sales_dict[customer]:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so) map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
delivery_note.insert(ignore_mandatory=True) delivery_note.flags.ignore_mandatory = True
delivery_note.insert()
update_packed_item_details(pick_list, delivery_note)
delivery_note.save()
return delivery_note return delivery_note
@ -520,28 +579,28 @@ def create_dn_with_so(sales_dict, pick_list):
def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
for location in pick_list.locations: for location in pick_list.locations:
if location.sales_order == sales_order: if location.sales_order != sales_order or location.product_bundle_item:
if location.sales_order_item: continue
sales_order_item = frappe.get_cached_doc(
"Sales Order Item", {"name": location.sales_order_item}
)
else:
sales_order_item = None
source_doc, table_mapper = ( if location.sales_order_item:
[sales_order_item, item_mapper] if sales_order_item else [location, item_mapper] sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item)
) else:
sales_order_item = None
dn_item = map_child_doc(source_doc, delivery_note, table_mapper) source_doc = sales_order_item or location
if dn_item: dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
dn_item.pick_list_item = location.name
dn_item.warehouse = location.warehouse
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
update_delivery_note_item(source_doc, dn_item, delivery_note) if dn_item:
dn_item.pick_list_item = location.name
dn_item.warehouse = location.warehouse
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
update_delivery_note_item(source_doc, dn_item, delivery_note)
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper)
set_delivery_note_missing_values(delivery_note) set_delivery_note_missing_values(delivery_note)
delivery_note.pick_list = pick_list.name delivery_note.pick_list = pick_list.name
@ -549,6 +608,50 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer") delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
def add_product_bundles_to_delivery_note(
pick_list: "PickList", delivery_note, item_mapper
) -> None:
"""Add product bundles found in pick list to delivery note.
When mapping pick list items, the bundle item itself isn't part of the
locations. Dynamically fetch and add parent bundle item into DN."""
product_bundles = pick_list._get_product_bundles()
product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
for so_row, item_code in product_bundles.items():
sales_order_item = frappe.get_doc("Sales Order Item", so_row)
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
so_row, product_bundle_qty_map[item_code]
)
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
"""Update stock details on packed items table of delivery note."""
def _find_so_row(packed_item):
for item in delivery_note.items:
if packed_item.parent_detail_docname == item.name:
return item.so_detail
def _find_pick_list_location(bundle_row, packed_item):
if not bundle_row:
return
for loc in pick_list.locations:
if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
return loc
for packed_item in delivery_note.packed_items:
so_row = _find_so_row(packed_item)
location = _find_pick_list_location(so_row, packed_item)
if not location:
continue
packed_item.warehouse = location.warehouse
packed_item.batch_no = location.batch_no
packed_item.serial_no = location.serial_no
@frappe.whitelist() @frappe.whitelist()
def create_stock_entry(pick_list): def create_stock_entry(pick_list):
pick_list = frappe.get_doc(json.loads(pick_list)) pick_list = frappe.get_doc(json.loads(pick_list))

@ -3,18 +3,21 @@
import frappe import frappe
from frappe import _dict from frappe import _dict
test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"]
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError, EmptyStockReconciliationItemsError,
) )
test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"]
class TestPickList(FrappeTestCase): class TestPickList(FrappeTestCase):
def test_pick_list_picks_warehouse_for_each_item(self): def test_pick_list_picks_warehouse_for_each_item(self):
@ -579,14 +582,79 @@ class TestPickList(FrappeTestCase):
if dn_item.item_code == "_Test Item 2": if dn_item.item_code == "_Test Item 2":
self.assertEqual(dn_item.qty, 2) self.assertEqual(dn_item.qty, 2)
# def test_pick_list_skips_items_in_expired_batch(self): def test_picklist_with_multi_uom(self):
# pass warehouse = "_Test Warehouse - _TC"
item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name
make_stock_entry(item=item, to_warehouse=warehouse, qty=1000)
# def test_pick_list_from_sales_order(self): so = make_sales_order(item_code=item, qty=10, rate=42, uom="Box")
# pass pl = create_pick_list(so.name)
# pick half the qty
for loc in pl.locations:
loc.picked_qty = loc.stock_qty / 2
pl.save()
pl.submit()
# def test_pick_list_from_work_order(self): so.reload()
# pass self.assertEqual(so.per_picked, 50)
# def test_pick_list_from_material_request(self): def test_picklist_with_bundles(self):
# pass warehouse = "_Test Warehouse - _TC"
quantities = [5, 2]
bundle, components = create_product_bundle(quantities, warehouse=warehouse)
bundle_items = dict(zip(components, quantities))
so = make_sales_order(item_code=bundle, qty=3, rate=42)
pl = create_pick_list(so.name)
pl.save()
self.assertEqual(len(pl.locations), 2)
for item in pl.locations:
self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3)
# check picking status on sales order
pl.submit()
so.reload()
self.assertEqual(so.per_picked, 100)
# deliver
dn = create_delivery_note(pl.name).submit()
self.assertEqual(dn.items[0].rate, 42)
self.assertEqual(dn.packed_items[0].warehouse, warehouse)
so.reload()
self.assertEqual(so.per_delivered, 100)
def test_picklist_with_partial_bundles(self):
# from test_records.json
warehouse = "_Test Warehouse - _TC"
quantities = [5, 2]
bundle, components = create_product_bundle(quantities, warehouse=warehouse)
so = make_sales_order(item_code=bundle, qty=4, rate=42)
pl = create_pick_list(so.name)
for loc in pl.locations:
loc.picked_qty = loc.qty / 2
pl.save().submit()
so.reload()
self.assertEqual(so.per_picked, 50)
# deliver half qty
dn = create_delivery_note(pl.name).submit()
self.assertEqual(dn.items[0].rate, 42)
so.reload()
self.assertEqual(so.per_delivered, 50)
pl = create_pick_list(so.name)
pl.save().submit()
so.reload()
self.assertEqual(so.per_picked, 100)
# deliver remaining
dn = create_delivery_note(pl.name).submit()
self.assertEqual(dn.items[0].rate, 42)
so.reload()
self.assertEqual(so.per_delivered, 100)

@ -27,6 +27,7 @@
"column_break_15", "column_break_15",
"sales_order", "sales_order",
"sales_order_item", "sales_order_item",
"product_bundle_item",
"material_request", "material_request",
"material_request_item" "material_request_item"
], ],
@ -146,6 +147,7 @@
{ {
"fieldname": "sales_order_item", "fieldname": "sales_order_item",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1,
"label": "Sales Order Item", "label": "Sales Order Item",
"read_only": 1 "read_only": 1
}, },
@ -177,11 +179,19 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Item Group", "label": "Item Group",
"read_only": 1 "read_only": 1
},
{
"description": "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle",
"fieldname": "product_bundle_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Product Bundle Item",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-09-28 12:02:16.923056", "modified": "2022-04-22 05:27:38.497997",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List Item", "name": "Pick List Item",
@ -190,5 +200,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

@ -1167,7 +1167,7 @@ class StockEntry(StockController):
from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s
where i.name=%s where i.name=%s
and i.disabled=0 and i.disabled=0
and (i.end_of_life is null or i.end_of_life='0000-00-00' or i.end_of_life > %s)""", and (i.end_of_life is null or i.end_of_life<'1900-01-01' or i.end_of_life > %s)""",
(self.company, args.get("item_code"), nowdate()), (self.company, args.get("item_code"), nowdate()),
as_dict=1, as_dict=1,
) )

@ -2,12 +2,12 @@
# See license.txt # See license.txt
import json import json
from datetime import timedelta
from uuid import uuid4 from uuid import uuid4
import frappe import frappe
from frappe.core.page.permission_manager.permission_manager import reset from frappe.core.page.permission_manager.permission_manager import reset
from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.query_builder.functions import CombineDatetime
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today from frappe.utils import add_days, today
from frappe.utils.data import add_to_date from frappe.utils.data import add_to_date
@ -1126,6 +1126,63 @@ class TestStockLedgerEntry(FrappeTestCase):
# original amount # original amount
self.assertEqual(50, _get_stock_credit(final_consumption)) self.assertEqual(50, _get_stock_credit(final_consumption))
def test_tie_breaking(self):
frappe.flags.dont_execute_stock_reposts = True
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
item = make_item().name
warehouse = "_Test Warehouse - _TC"
posting_date = "2022-01-01"
posting_time = "00:00:01"
sle = frappe.qb.DocType("Stock Ledger Entry")
def ordered_qty_after_transaction():
return (
frappe.qb.from_(sle)
.select("qty_after_transaction")
.where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.creation)
).run(pluck=True)
first = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=10,
posting_time=posting_time,
posting_date=posting_date,
do_not_submit=True,
)
second = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=1,
posting_date=posting_date,
posting_time=posting_time,
do_not_submit=True,
)
first.submit()
second.submit()
self.assertEqual([10, 11], ordered_qty_after_transaction())
first.cancel()
self.assertEqual([1], ordered_qty_after_transaction())
backdated = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=1,
posting_date="2021-01-01",
posting_time=posting_time,
)
self.assertEqual([1, 2], ordered_qty_after_transaction())
backdated.cancel()
self.assertEqual([1], ordered_qty_after_transaction())
def create_repack_entry(**args): def create_repack_entry(**args):
args = frappe._dict(args) args = frappe._dict(args)

@ -30,7 +30,6 @@ class TestStockReconciliation(FrappeTestCase):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
def tearDown(self): def tearDown(self):
frappe.flags.dont_execute_stock_reposts = None
frappe.local.future_sle = {} frappe.local.future_sle = {}
def test_reco_for_fifo(self): def test_reco_for_fifo(self):
@ -40,7 +39,9 @@ class TestStockReconciliation(FrappeTestCase):
self._test_reco_sle_gle("Moving Average") self._test_reco_sle_gle("Moving Average")
def _test_reco_sle_gle(self, valuation_method): def _test_reco_sle_gle(self, valuation_method):
se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1") item_code = make_item(properties={"valuation_method": valuation_method}).name
se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code)
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
# [[qty, valuation_rate, posting_date, # [[qty, valuation_rate, posting_date,
# posting_time, expected_stock_value, bin_qty, bin_valuation]] # posting_time, expected_stock_value, bin_qty, bin_valuation]]
@ -54,11 +55,9 @@ class TestStockReconciliation(FrappeTestCase):
] ]
for d in input_data: for d in input_data:
set_valuation_method("_Test Item", valuation_method)
last_sle = get_previous_sle( last_sle = get_previous_sle(
{ {
"item_code": "_Test Item", "item_code": item_code,
"warehouse": "Stores - TCP1", "warehouse": "Stores - TCP1",
"posting_date": d[2], "posting_date": d[2],
"posting_time": d[3], "posting_time": d[3],
@ -67,6 +66,7 @@ class TestStockReconciliation(FrappeTestCase):
# submit stock reconciliation # submit stock reconciliation
stock_reco = create_stock_reconciliation( stock_reco = create_stock_reconciliation(
item_code=item_code,
qty=d[0], qty=d[0],
rate=d[1], rate=d[1],
posting_date=d[2], posting_date=d[2],
@ -481,9 +481,11 @@ class TestStockReconciliation(FrappeTestCase):
""" """
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
frappe.db.rollback()
# repost will make this test useless, qty should update in realtime without reposts # repost will make this test useless, qty should update in realtime without reposts
frappe.flags.dont_execute_stock_reposts = True frappe.flags.dont_execute_stock_reposts = True
frappe.db.rollback() self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
item_code = make_item().name item_code = make_item().name
warehouse = "_Test Warehouse - _TC" warehouse = "_Test Warehouse - _TC"
@ -594,26 +596,26 @@ def create_batch_item_with_batch(item_name, batch_id):
b.save() b.save()
def insert_existing_sle(warehouse): def insert_existing_sle(warehouse, item_code="_Test Item"):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
se1 = make_stock_entry( se1 = make_stock_entry(
posting_date="2012-12-15", posting_date="2012-12-15",
posting_time="02:00", posting_time="02:00",
item_code="_Test Item", item_code=item_code,
target=warehouse, target=warehouse,
qty=10, qty=10,
basic_rate=700, basic_rate=700,
) )
se2 = make_stock_entry( se2 = make_stock_entry(
posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", source=warehouse, qty=15 posting_date="2012-12-25", posting_time="03:00", item_code=item_code, source=warehouse, qty=15
) )
se3 = make_stock_entry( se3 = make_stock_entry(
posting_date="2013-01-05", posting_date="2013-01-05",
posting_time="07:00", posting_time="07:00",
item_code="_Test Item", item_code=item_code,
target=warehouse, target=warehouse,
qty=15, qty=15,
basic_rate=1200, basic_rate=1200,

@ -36,6 +36,9 @@ class Warehouse(NestedSet):
self.set_onload("account", account) self.set_onload("account", account)
load_address_and_contact(self) load_address_and_contact(self)
def validate(self):
self.warn_about_multiple_warehouse_account()
def on_update(self): def on_update(self):
self.update_nsm_model() self.update_nsm_model()
@ -70,6 +73,53 @@ class Warehouse(NestedSet):
self.update_nsm_model() self.update_nsm_model()
self.unlink_from_items() self.unlink_from_items()
def warn_about_multiple_warehouse_account(self):
"If Warehouse value is split across multiple accounts, warn."
def get_accounts_where_value_is_booked(name):
sle = frappe.qb.DocType("Stock Ledger Entry")
gle = frappe.qb.DocType("GL Entry")
ac = frappe.qb.DocType("Account")
return (
frappe.qb.from_(sle)
.join(gle)
.on(sle.voucher_no == gle.voucher_no)
.join(ac)
.on(ac.name == gle.account)
.select(gle.account)
.distinct()
.where((sle.warehouse == name) & (ac.account_type == "Stock"))
.orderby(sle.creation)
.run(as_dict=True)
)
if self.is_new():
return
old_wh_account = frappe.db.get_value("Warehouse", self.name, "account")
# WH account is being changed or set get all accounts against which wh value is booked
if self.account != old_wh_account:
accounts = get_accounts_where_value_is_booked(self.name)
accounts = [d.account for d in accounts]
if not accounts or (len(accounts) == 1 and self.account in accounts):
# if same singular account has stock value booked ignore
return
warning = _("Warehouse's Stock Value has already been booked in the following accounts:")
account_str = "<br>" + ", ".join(frappe.bold(ac) for ac in accounts)
reason = "<br><br>" + _(
"Booking stock value across multiple accounts will make it harder to track stock and account value."
)
frappe.msgprint(
warning + account_str + reason,
title=_("Multiple Warehouse Accounts"),
indicator="orange",
)
def check_if_sle_exists(self): def check_if_sle_exists(self):
return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name}) return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})

@ -8,9 +8,8 @@ from typing import Optional, Set, Tuple
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
from pypika import CustomFunction
import erpnext import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@ -1158,16 +1157,15 @@ def get_batch_incoming_rate(
item_code, warehouse, batch_no, posting_date, posting_time, creation=None item_code, warehouse, batch_no, posting_date, posting_time, creation=None
): ):
Timestamp = CustomFunction("timestamp", ["date", "time"])
sle = frappe.qb.DocType("Stock Ledger Entry") sle = frappe.qb.DocType("Stock Ledger Entry")
timestamp_condition = Timestamp(sle.posting_date, sle.posting_time) < Timestamp( timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
posting_date, posting_time posting_date, posting_time
) )
if creation: if creation:
timestamp_condition |= ( timestamp_condition |= (
Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time) CombineDatetime(sle.posting_date, sle.posting_time)
== CombineDatetime(posting_date, posting_time)
) & (sle.creation < creation) ) & (sle.creation < creation)
batch_details = ( batch_details = (

@ -7,6 +7,7 @@ from typing import Dict, Optional
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import CombineDatetime
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
import erpnext import erpnext
@ -143,12 +144,10 @@ def get_stock_balance(
def get_serial_nos_data_after_transactions(args): def get_serial_nos_data_after_transactions(args):
from pypika import CustomFunction
serial_nos = set() serial_nos = set()
args = frappe._dict(args) args = frappe._dict(args)
sle = frappe.qb.DocType("Stock Ledger Entry") sle = frappe.qb.DocType("Stock Ledger Entry")
Timestamp = CustomFunction("timestamp", ["date", "time"])
stock_ledger_entries = ( stock_ledger_entries = (
frappe.qb.from_(sle) frappe.qb.from_(sle)
@ -157,7 +156,8 @@ def get_serial_nos_data_after_transactions(args):
(sle.item_code == args.item_code) (sle.item_code == args.item_code)
& (sle.warehouse == args.warehouse) & (sle.warehouse == args.warehouse)
& ( & (
Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time) CombineDatetime(sle.posting_date, sle.posting_time)
< CombineDatetime(args.posting_date, args.posting_time)
) )
& (sle.is_cancelled == 0) & (sle.is_cancelled == 0)
) )

@ -3034,7 +3034,7 @@ To Date,До,
To Date cannot be before From Date,На сегодняшний день не может быть раньше от даты, To Date cannot be before From Date,На сегодняшний день не может быть раньше от даты,
To Date cannot be less than From Date,"Дата не может быть меньше, чем с даты", To Date cannot be less than From Date,"Дата не может быть меньше, чем с даты",
To Date must be greater than From Date,"До даты должно быть больше, чем с даты", To Date must be greater than From Date,"До даты должно быть больше, чем с даты",
To Date should be within the Fiscal Year. Assuming To Date = {0},Дата должна быть в пределах финансового года. Предположим, до даты = {0}, "To Date should be within the Fiscal Year. Assuming To Date = {0}","Дата должна быть в пределах финансового года. Предположим, до даты = {0}",
To Datetime,Для DateTime, To Datetime,Для DateTime,
To Deliver,Для доставки, To Deliver,Для доставки,
To Deliver and Bill,Для доставки и оплаты, To Deliver and Bill,Для доставки и оплаты,
@ -6917,7 +6917,7 @@ Time after the end of shift during which check-out is considered for attendance.
Working Hours Threshold for Half Day,Порог рабочего времени на полдня, Working Hours Threshold for Half Day,Порог рабочего времени на полдня,
Working hours below which Half Day is marked. (Zero to disable),"Рабочее время, ниже которого отмечается полдня. (Ноль отключить)", Working hours below which Half Day is marked. (Zero to disable),"Рабочее время, ниже которого отмечается полдня. (Ноль отключить)",
Working Hours Threshold for Absent,Порог рабочего времени для отсутствующих, Working Hours Threshold for Absent,Порог рабочего времени для отсутствующих,
Working hours below which Absent is marked. (Zero to disable),Порог рабочего времени, ниже которого устанавливается отметка «Отсутствует». (Ноль для отключения),", "Working hours below which Absent is marked. (Zero to disable)","Порог рабочего времени, ниже которого устанавливается отметка «Отсутствует». (Ноль для отключения)",
Process Attendance After,Посещаемость процесса после, Process Attendance After,Посещаемость процесса после,
Attendance will be marked automatically only after this date.,Посещаемость будет отмечена автоматически только после этой даты., Attendance will be marked automatically only after this date.,Посещаемость будет отмечена автоматически только после этой даты.,
Last Sync of Checkin,Последняя синхронизация регистрации, Last Sync of Checkin,Последняя синхронизация регистрации,

Can't render this file because it is too large.