Merge branch 'develop' into item-attr-abbr-lowercase

This commit is contained in:
Sagar Sharma 2023-01-19 12:55:49 +05:30 committed by GitHub
commit 333b2ae721
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 382 additions and 203 deletions

View File

@ -66,7 +66,8 @@ ignore =
F841, F841,
E713, E713,
E712, E712,
B023 B023,
B028
max-line-length = 200 max-line-length = 200

View File

@ -6,6 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"api_details_section", "api_details_section",
"disabled",
"service_provider", "service_provider",
"api_endpoint", "api_endpoint",
"url", "url",
@ -77,12 +78,18 @@
"label": "Service Provider", "label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom", "options": "frankfurter.app\nexchangerate.host\nCustom",
"reqd": 1 "reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-01-10 15:51:14.521174", "modified": "2023-01-09 12:19:03.955906",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Currency Exchange Settings", "name": "Currency Exchange Settings",

View File

@ -137,8 +137,7 @@
"fieldname": "finance_book", "fieldname": "finance_book",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Finance Book", "label": "Finance Book",
"options": "Finance Book", "options": "Finance Book"
"read_only": 1
}, },
{ {
"fieldname": "2_add_edit_gl_entries", "fieldname": "2_add_edit_gl_entries",
@ -539,7 +538,7 @@
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-28 17:40:01.241908", "modified": "2023-01-17 12:53:53.280620",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@ -334,7 +334,7 @@ class PaymentReconciliation(Document):
) )
# Account Currency has balance # Account Currency has balance
dr_or_cr = "debit" if self.party_type == "Customer" else "debit" dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account = frappe._dict( journal_account = frappe._dict(
@ -471,6 +471,7 @@ class PaymentReconciliation(Document):
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear() self.common_filter_conditions.clear()
self.accounting_dimension_filter_conditions.clear()
self.ple_posting_date_filter.clear() self.ple_posting_date_filter.clear()
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")

View File

@ -49,7 +49,6 @@
<br> <br>
{% endif %} {% endif %}
{{ _("Against") }}: {{ row.against }}
<br>{{ _("Remarks") }}: {{ row.remarks }} <br>{{ _("Remarks") }}: {{ row.remarks }}
{% if row.bill_no %} {% if row.bill_no %}
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }} <br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}

View File

@ -7,7 +7,6 @@ import frappe
from frappe import _, qb from frappe import _, qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.utils.background_jobs import is_job_queued
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
@ -27,7 +26,7 @@ def start_payment_ledger_repost(docname=None):
""" """
if docname: if docname:
repost_doc = frappe.get_doc("Repost Payment Ledger", docname) repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
if repost_doc.docstatus == 1 and repost_doc.repost_status in ["Queued", "Failed"]: if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]:
try: try:
for entry in repost_doc.repost_vouchers: for entry in repost_doc.repost_vouchers:
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
@ -102,7 +101,6 @@ def execute_repost_payment_ledger(docname):
job_name = "payment_ledger_repost_" + docname job_name = "payment_ledger_repost_" + docname
if not is_job_queued(job_name):
frappe.enqueue( frappe.enqueue(
method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost", method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
docname=docname, docname=docname,

View File

@ -259,9 +259,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if tax_deducted: if tax_deducted:
net_total = inv.tax_withholding_net_total net_total = inv.tax_withholding_net_total
if ldc: if ldc:
tax_amount = get_tds_amount_from_ldc( tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total)
ldc, parties, pan_no, tax_details, posting_date, net_total
)
else: else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
@ -538,7 +536,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
return inv.grand_total - tcs_tax_row_amount return inv.grand_total - tcs_tax_row_amount
def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total): def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
tds_amount = 0 tds_amount = 0
limit_consumed = frappe.db.get_value( limit_consumed = frappe.db.get_value(
"Purchase Invoice", "Purchase Invoice",

View File

@ -25,8 +25,8 @@
<thead> <thead>
<tr> <tr>
<th style="width: 12%">{%= __("Date") %}</th> <th style="width: 12%">{%= __("Date") %}</th>
<th style="width: 15%">{%= __("Ref") %}</th> <th style="width: 15%">{%= __("Reference") %}</th>
<th style="width: 25%">{%= __("Party") %}</th> <th style="width: 25%">{%= __("Remarks") %}</th>
<th style="width: 15%">{%= __("Debit") %}</th> <th style="width: 15%">{%= __("Debit") %}</th>
<th style="width: 15%">{%= __("Credit") %}</th> <th style="width: 15%">{%= __("Credit") %}</th>
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th> <th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
@ -45,7 +45,6 @@
<br> <br>
{% } %} {% } %}
{{ __("Against") }}: {%= data[i].against %}
<br>{%= __("Remarks") %}: {%= data[i].remarks %} <br>{%= __("Remarks") %}: {%= data[i].remarks %}
{% if(data[i].bill_no) { %} {% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} <br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}

View File

@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt
def execute(filters=None): def execute(filters=None):
@ -65,6 +66,12 @@ def get_result(
else: else:
total_amount_credited += entry.credit total_amount_credited += entry.credit
## Check if ldc is applied and show rate as per ldc
actual_rate = (tds_deducted / total_amount_credited) * 100
if flt(actual_rate) < flt(rate):
rate = actual_rate
if tds_deducted: if tds_deducted:
row = { row = {
"pan" "pan"

View File

@ -439,8 +439,7 @@ def reconcile_against_document(args): # nosemgrep
# cancel advance entry # cancel advance entry
doc = frappe.get_doc(voucher_type, voucher_no) doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True frappe.flags.ignore_party_validation = True
gl_map = doc.build_gl_map() _delete_pl_entries(voucher_type, voucher_no)
create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1)
for entry in entries: for entry in entries:
check_if_advance_entry_modified(entry) check_if_advance_entry_modified(entry)
@ -452,11 +451,23 @@ def reconcile_against_document(args): # nosemgrep
else: else:
update_reference_in_payment_entry(entry, doc, do_not_save=True) update_reference_in_payment_entry(entry, doc, do_not_save=True)
if doc.doctype == "Journal Entry":
try:
doc.validate_total_debit_and_credit()
except Exception as validation_exception:
raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
# re-submit advance entry # re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
gl_map = doc.build_gl_map() gl_map = doc.build_gl_map()
create_payment_ledger_entry(gl_map, cancel=0, adv_adj=1) create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
# Only update outstanding for newly linked vouchers
for entry in entries:
update_voucher_outstanding(
entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party
)
frappe.flags.ignore_party_validation = False frappe.flags.ignore_party_validation = False

View File

@ -135,6 +135,10 @@ frappe.ui.form.on('Asset', {
}, __("Manage")); }, __("Manage"));
} }
if (frm.doc.depr_entry_posting_status === "Failed") {
frm.trigger("set_depr_posting_failure_alert");
}
frm.trigger("setup_chart"); frm.trigger("setup_chart");
} }
@ -145,6 +149,19 @@ frappe.ui.form.on('Asset', {
} }
}, },
set_depr_posting_failure_alert: function (frm) {
const alert = `
<div class="row">
<div class="col-xs-12 col-sm-6">
<span class="indicator whitespace-nowrap red">
<span>Failed to post depreciation entries</span>
</span>
</div>
</div>`;
frm.dashboard.set_headline_alert(alert);
},
toggle_reference_doc: function(frm) { toggle_reference_doc: function(frm) {
if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) { if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) {
frm.set_df_property('purchase_invoice', 'read_only', 1); frm.set_df_property('purchase_invoice', 'read_only', 1);

View File

@ -68,6 +68,7 @@
"column_break_51", "column_break_51",
"purchase_receipt_amount", "purchase_receipt_amount",
"default_finance_book", "default_finance_book",
"depr_entry_posting_status",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -473,6 +474,16 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Asset Quantity", "label": "Asset Quantity",
"read_only_depends_on": "eval:!doc.is_existing_asset" "read_only_depends_on": "eval:!doc.is_existing_asset"
},
{
"fieldname": "depr_entry_posting_status",
"fieldtype": "Select",
"hidden": 1,
"label": "Depreciation Entry Posting Status",
"no_copy": 1,
"options": "\nSuccessful\nFailed",
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 72, "idx": 72,
@ -487,7 +498,7 @@
{ {
"group": "Repair", "group": "Repair",
"link_doctype": "Asset Repair", "link_doctype": "Asset Repair",
"link_fieldname": "asset_name" "link_fieldname": "asset"
}, },
{ {
"group": "Value", "group": "Value",
@ -500,7 +511,7 @@
"link_fieldname": "asset" "link_fieldname": "asset"
} }
], ],
"modified": "2022-11-25 12:47:19.689702", "modified": "2023-01-17 00:25:30.387242",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today
from frappe.utils.user import get_users_with_role
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, get_checks_for_pl_and_bs_accounts,
@ -18,7 +19,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
) )
def post_depreciation_entries(date=None, commit=True): def post_depreciation_entries(date=None):
# Return if automatic booking of asset depreciation is disabled # Return if automatic booking of asset depreciation is disabled
if not cint( if not cint(
frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically") frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically")
@ -27,12 +28,23 @@ def post_depreciation_entries(date=None, commit=True):
if not date: if not date:
date = today() date = today()
failed_asset_names = []
for asset_name in get_depreciable_assets(date): for asset_name in get_depreciable_assets(date):
asset_doc = frappe.get_doc("Asset", asset_name) asset_doc = frappe.get_doc("Asset", asset_name)
try:
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
failed_asset_names.append(asset_name)
if failed_asset_names:
set_depr_entry_posting_status_for_failed_assets(failed_asset_names)
notify_depr_entry_posting_error(failed_asset_names)
if commit:
frappe.db.commit() frappe.db.commit()
@ -146,6 +158,8 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None):
row.value_after_depreciation -= d.depreciation_amount row.value_after_depreciation -= d.depreciation_amount
row.db_update() row.db_update()
frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful")
asset.set_status() asset.set_status()
return asset_depr_schedule_doc return asset_depr_schedule_doc
@ -209,6 +223,42 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation
return credit_account, debit_account return credit_account, debit_account
def set_depr_entry_posting_status_for_failed_assets(failed_asset_names):
for asset_name in failed_asset_names:
frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Failed")
def notify_depr_entry_posting_error(failed_asset_names):
recipients = get_users_with_role("Accounts Manager")
if not recipients:
recipients = get_users_with_role("System Manager")
subject = _("Error while posting depreciation entries")
asset_links = get_comma_separated_asset_links(failed_asset_names)
message = (
_("Hi,")
+ "<br>"
+ _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
+ "."
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
def get_comma_separated_asset_links(asset_names):
asset_links = []
for asset_name in asset_names:
asset_links.append(get_link_to_form("Asset", asset_name))
asset_links = ", ".join(asset_links)
return asset_links
@frappe.whitelist() @frappe.whitelist()
def scrap_asset(asset_name): def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
@ -295,12 +345,12 @@ def reset_depreciation_schedule(asset_doc, date, notes):
asset_doc, notes, date_of_return=date asset_doc, notes, date_of_return=date
) )
modify_depreciation_schedule_for_asset_repairs(asset_doc) modify_depreciation_schedule_for_asset_repairs(asset_doc, notes)
asset_doc.save() asset_doc.save()
def modify_depreciation_schedule_for_asset_repairs(asset): def modify_depreciation_schedule_for_asset_repairs(asset, notes):
asset_repairs = frappe.get_all( asset_repairs = frappe.get_all(
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
) )
@ -309,10 +359,6 @@ def modify_depreciation_schedule_for_asset_repairs(asset):
if repair.increase_in_asset_life: if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name) asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule() asset_repair.modify_depreciation_schedule()
notes = _("This schedule was created when Asset {0} went through Asset Repair {1}.").format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(asset_repair.doctype, asset_repair.name),
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes) make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)

View File

@ -1549,6 +1549,7 @@ def create_asset(**args):
"asset_owner": args.asset_owner or "Company", "asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1, "is_existing_asset": args.is_existing_asset or 1,
"asset_quantity": args.get("asset_quantity") or 1, "asset_quantity": args.get("asset_quantity") or 1,
"depr_entry_posting_status": args.depr_entry_posting_status or "",
} }
) )

View File

@ -8,7 +8,6 @@ import frappe
# import erpnext # import erpnext
from frappe import _ from frappe import _
from frappe.utils import cint, flt, get_link_to_form from frappe.utils import cint, flt, get_link_to_form
from six import string_types
import erpnext import erpnext
from erpnext.assets.doctype.asset.depreciation import ( from erpnext.assets.doctype.asset.depreciation import (
@ -431,7 +430,7 @@ class AssetCapitalization(StockController):
if asset.calculate_depreciation: if asset.calculate_depreciation:
notes = _( notes = _(
"This schedule was created when Asset {0} was consumed when Asset Capitalization {1} was submitted." "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
).format( ).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.get("name")) get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.get("name"))
) )
@ -522,7 +521,7 @@ class AssetCapitalization(StockController):
asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value
notes = _( notes = _(
"This schedule was created when target Asset {0} was updated when Asset Capitalization {1} was submitted." "This schedule was created when target Asset {0} was updated through Asset Capitalization {1}."
).format( ).format(
get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name) get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name)
) )
@ -538,7 +537,7 @@ class AssetCapitalization(StockController):
if asset.calculate_depreciation: if asset.calculate_depreciation:
reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
notes = _( notes = _(
"This schedule was created when Asset {0} was restored when Asset Capitalization {1} was cancelled." "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation."
).format( ).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name)
) )
@ -626,7 +625,7 @@ def get_target_asset_details(asset=None, company=None):
@frappe.whitelist() @frappe.whitelist()
def get_consumed_stock_item_details(args): def get_consumed_stock_item_details(args):
if isinstance(args, string_types): if isinstance(args, str):
args = json.loads(args) args = json.loads(args)
args = frappe._dict(args) args = frappe._dict(args)
@ -678,7 +677,7 @@ def get_consumed_stock_item_details(args):
@frappe.whitelist() @frappe.whitelist()
def get_warehouse_details(args): def get_warehouse_details(args):
if isinstance(args, string_types): if isinstance(args, str):
args = json.loads(args) args = json.loads(args)
args = frappe._dict(args) args = frappe._dict(args)
@ -694,7 +693,7 @@ def get_warehouse_details(args):
@frappe.whitelist() @frappe.whitelist()
def get_consumed_asset_details(args): def get_consumed_asset_details(args):
if isinstance(args, string_types): if isinstance(args, str):
args = json.loads(args) args = json.loads(args)
args = frappe._dict(args) args = frappe._dict(args)
@ -746,7 +745,7 @@ def get_consumed_asset_details(args):
@frappe.whitelist() @frappe.whitelist()
def get_service_item_details(args): def get_service_item_details(args):
if isinstance(args, string_types): if isinstance(args, str):
args = json.loads(args) args = json.loads(args)
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -159,7 +159,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-02 15:38:30.766779", "modified": "2023-01-16 21:08:21.421260",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Depreciation Schedule", "name": "Asset Depreciation Schedule",

View File

@ -56,8 +56,11 @@ class AssetRepair(AccountsController):
): ):
self.modify_depreciation_schedule() self.modify_depreciation_schedule()
notes = _("This schedule was created when Asset Repair {0} was submitted.").format( notes = _(
get_link_to_form(self.doctype, self.name) "This schedule was created when Asset {0} was repaired through Asset Repair {1}."
).format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
) )
self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
@ -80,8 +83,9 @@ class AssetRepair(AccountsController):
): ):
self.revert_depreciation_schedule_on_cancellation() self.revert_depreciation_schedule_on_cancellation()
notes = _("This schedule was created when Asset Repair {0} was cancelled.").format( notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format(
get_link_to_form(self.doctype, self.name) get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
) )
self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)

View File

@ -127,12 +127,20 @@ class AssetValueAdjustment(Document):
current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
current_asset_depr_schedule_doc.cancel() current_asset_depr_schedule_doc.cancel()
if self.docstatus == 1:
notes = _( notes = _(
"This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
).format( ).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.get("doctype"), self.get("name")), get_link_to_form(self.get("doctype"), self.get("name")),
) )
elif self.docstatus == 2:
notes = _(
"This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.get("doctype"), self.get("name")),
)
new_asset_depr_schedule_doc.notes = notes new_asset_depr_schedule_doc.notes = notes
new_asset_depr_schedule_doc.insert() new_asset_depr_schedule_doc.insert()

View File

@ -889,6 +889,11 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(po.status, "Completed") self.assertEqual(po.status, "Completed")
self.assertEqual(mr.status, "Received") self.assertEqual(mr.status, "Received")
def test_variant_item_po(self):
po = create_purchase_order(item_code="_Test Variant Item", qty=1, rate=100, do_not_save=1)
self.assertRaises(frappe.ValidationError, po.save)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@ -994,8 +999,8 @@ def create_purchase_order(**args):
}, },
) )
po.set_missing_values()
if not args.do_not_save: if not args.do_not_save:
po.set_missing_values()
po.insert() po.insert()
if not args.do_not_submit: if not args.do_not_submit:
if po.is_subcontracted: if po.is_subcontracted:

View File

@ -394,7 +394,7 @@ class AccountsController(TransactionBase):
self.get("inter_company_reference") self.get("inter_company_reference")
or self.get("inter_company_invoice_reference") or self.get("inter_company_invoice_reference")
or self.get("inter_company_order_reference") or self.get("inter_company_order_reference")
): ) and not self.get("is_return"):
msg = _("Internal Sale or Delivery Reference missing.") msg = _("Internal Sale or Delivery Reference missing.")
msg += _("Please create purchase from internal sale or delivery document itself") msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing")) frappe.throw(msg, title=_("Internal Sales Reference Missing"))

View File

@ -37,7 +37,7 @@ def validate_return_against(doc):
if ( if (
ref_doc.company == doc.company ref_doc.company == doc.company
and ref_doc.get(party_type) == doc.get(party_type) and ref_doc.get(party_type) == doc.get(party_type)
and ref_doc.docstatus == 1 and ref_doc.docstatus.is_submitted()
): ):
# validate posting date time # validate posting date time
return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")

View File

@ -6,6 +6,7 @@ import json
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
import erpnext import erpnext
@ -20,7 +21,7 @@ from erpnext.stock.get_item_details import _get_item_tax_template
class calculate_taxes_and_totals(object): class calculate_taxes_and_totals(object):
def __init__(self, doc): def __init__(self, doc: Document):
self.doc = doc self.doc = doc
frappe.flags.round_off_applicable_accounts = [] frappe.flags.round_off_applicable_accounts = []
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
@ -677,7 +678,7 @@ class calculate_taxes_and_totals(object):
) )
def calculate_total_advance(self): def calculate_total_advance(self):
if self.doc.docstatus < 2: if not self.doc.docstatus.is_cancelled():
total_allocated_amount = sum( total_allocated_amount = sum(
flt(adv.allocated_amount, adv.precision("allocated_amount")) flt(adv.allocated_amount, adv.precision("allocated_amount"))
for adv in self.doc.get("advances") for adv in self.doc.get("advances")
@ -708,7 +709,7 @@ class calculate_taxes_and_totals(object):
) )
) )
if self.doc.docstatus == 0: if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"): if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0 self.doc.write_off_amount = 0

View File

@ -13,38 +13,24 @@ frappe.query_reports["Work Order Summary"] = {
reqd: 1 reqd: 1
}, },
{ {
fieldname: "fiscal_year", label: __("Based On"),
label: __("Fiscal Year"), fieldname:"based_on",
fieldtype: "Link", fieldtype: "Select",
options: "Fiscal Year", options: "Creation Date\nPlanned Date\nActual Date",
default: frappe.defaults.get_user_default("fiscal_year"), default: "Creation Date"
reqd: 1,
on_change: function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year;
if (!fiscal_year) {
return;
}
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
from_date: fy.year_start_date,
to_date: fy.year_end_date
});
});
}
}, },
{ {
label: __("From Posting Date"), label: __("From Posting Date"),
fieldname:"from_date", fieldname:"from_date",
fieldtype: "Date", fieldtype: "Date",
default: frappe.defaults.get_user_default("year_start_date"), default: frappe.datetime.add_months(frappe.datetime.get_today(), -3),
reqd: 1 reqd: 1
}, },
{ {
label: __("To Posting Date"), label: __("To Posting Date"),
fieldname:"to_date", fieldname:"to_date",
fieldtype: "Date", fieldtype: "Date",
default: frappe.defaults.get_user_default("year_end_date"), default: frappe.datetime.get_today(),
reqd: 1, reqd: 1,
}, },
{ {

View File

@ -31,6 +31,7 @@ def get_data(filters):
"sales_order", "sales_order",
"production_item", "production_item",
"qty", "qty",
"creation",
"produced_qty", "produced_qty",
"planned_start_date", "planned_start_date",
"planned_end_date", "planned_end_date",
@ -47,11 +48,17 @@ def get_data(filters):
if filters.get(field): if filters.get(field):
query_filters[field] = filters.get(field) query_filters[field] = filters.get(field)
if filters.get("based_on") == "Planned Date":
query_filters["planned_start_date"] = (">=", filters.get("from_date")) query_filters["planned_start_date"] = (">=", filters.get("from_date"))
query_filters["planned_end_date"] = ("<=", filters.get("to_date")) query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
elif filters.get("based_on") == "Actual Date":
query_filters["actual_start_date"] = (">=", filters.get("from_date"))
query_filters["actual_end_date"] = ("<=", filters.get("to_date"))
else:
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
data = frappe.get_all( data = frappe.get_all(
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc" "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1
) )
res = [] res = []
@ -213,6 +220,12 @@ def get_columns(filters):
"options": "Sales Order", "options": "Sales Order",
"width": 90, "width": 90,
}, },
{
"label": _("Created On"),
"fieldname": "creation",
"fieldtype": "Date",
"width": 150,
},
{ {
"label": _("Planned Start Date"), "label": _("Planned Start Date"),
"fieldname": "planned_start_date", "fieldname": "planned_start_date",

View File

@ -194,7 +194,6 @@ erpnext.patches.v13_0.update_project_template_tasks
erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.convert_qi_parameter_to_link_field
erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021 erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
erpnext.patches.v13_0.update_payment_terms_outstanding erpnext.patches.v13_0.update_payment_terms_outstanding
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
@ -269,6 +268,7 @@ erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v15_0.delete_taxjar_doctypes erpnext.patches.v15_0.delete_taxjar_doctypes
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
[post_model_sync] [post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
@ -291,6 +291,7 @@ erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.delete_amazon_mws_doctype erpnext.patches.v14_0.delete_amazon_mws_doctype
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.update_batch_valuation_flag
erpnext.patches.v14_0.delete_non_profit_doctypes erpnext.patches.v14_0.delete_non_profit_doctypes
erpnext.patches.v13_0.add_cost_center_in_loans erpnext.patches.v13_0.add_cost_center_in_loans

View File

@ -7,6 +7,7 @@ from erpnext.stock.stock_ledger import update_entries_after
def execute(): def execute():
doctypes_to_reload = [ doctypes_to_reload = [
("setup", "company"),
("stock", "repost_item_valuation"), ("stock", "repost_item_valuation"),
("stock", "stock_entry_detail"), ("stock", "stock_entry_detail"),
("stock", "purchase_receipt_item"), ("stock", "purchase_receipt_item"),

View File

@ -0,0 +1,12 @@
import frappe
def execute():
if frappe.db.get_value("Journal Entry Account", {"reference_due_date": ""}):
frappe.db.sql(
"""
UPDATE `tabJournal Entry Account`
SET reference_due_date = NULL
WHERE reference_due_date = ''
"""
)

View File

@ -80,7 +80,7 @@ class Task(NestedSet):
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
frappe.throw( frappe.throw(
_( _(
"Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled." "Cannot complete task {0} as its dependant task {1} are not completed / cancelled."
).format(frappe.bold(self.name), frappe.bold(d.task)) ).format(frappe.bold(self.name), frappe.bold(d.task))
) )

View File

@ -1,7 +1,7 @@
frappe.provide("erpnext.accounts.bank_reconciliation"); frappe.provide("erpnext.accounts.bank_reconciliation");
erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
constructor(company, bank_account) { constructor(company, bank_account, bank_statement_from_date, bank_statement_to_date, filter_by_reference_date, from_reference_date, to_reference_date) {
this.bank_account = bank_account; this.bank_account = bank_account;
this.company = company; this.company = company;
this.make_dialog(); this.make_dialog();

View File

@ -194,14 +194,7 @@ def get_list_context(context=None):
@frappe.whitelist() @frappe.whitelist()
def make_sales_order(source_name, target_doc=None): def make_sales_order(source_name: str, target_doc=None):
quotation = frappe.db.get_value(
"Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
)
if quotation.valid_till and (
quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
):
frappe.throw(_("Validity period of this quotation has ended."))
return _make_sales_order(source_name, target_doc) return _make_sales_order(source_name, target_doc)

View File

@ -136,17 +136,20 @@ class TestQuotation(FrappeTestCase):
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30)) sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
) )
def test_valid_till(self): def test_valid_till_before_transaction_date(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
quotation = frappe.copy_doc(test_records[0]) quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(quotation.transaction_date, -1) quotation.valid_till = add_days(quotation.transaction_date, -1)
self.assertRaises(frappe.ValidationError, quotation.validate) self.assertRaises(frappe.ValidationError, quotation.validate)
def test_so_from_expired_quotation(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(nowdate(), -1) quotation.valid_till = add_days(nowdate(), -1)
quotation.insert() quotation.insert()
quotation.submit() quotation.submit()
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
make_sales_order(quotation.name)
def test_shopping_cart_without_website_item(self): def test_shopping_cart_without_website_item(self):
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}): if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):

View File

@ -208,7 +208,7 @@ class SalesOrder(SellingController):
for quotation in set(d.prevdoc_docname for d in self.get("items")): for quotation in set(d.prevdoc_docname for d in self.get("items")):
if quotation: if quotation:
doc = frappe.get_doc("Quotation", quotation) doc = frappe.get_doc("Quotation", quotation)
if doc.docstatus == 2: if doc.docstatus.is_cancelled():
frappe.throw(_("Quotation {0} is cancelled").format(quotation)) frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True) doc.set_status(update=True)

View File

@ -14,7 +14,6 @@ def get_data():
}, },
"internal_links": { "internal_links": {
"Quotation": ["items", "prevdoc_docname"], "Quotation": ["items", "prevdoc_docname"],
"Material Request": ["items", "material_request"],
}, },
"transactions": [ "transactions": [
{ {

View File

@ -638,7 +638,6 @@
"width": "70px" "width": "70px"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "ordered_qty", "fieldname": "ordered_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Ordered Qty", "label": "Ordered Qty",
@ -865,7 +864,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-01-12 13:13:28.691585", "modified": "2022-12-25 02:51:10.247569",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@ -81,6 +81,11 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if entries: if entries:
return flt(entries[0].exchange_rate) return flt(entries[0].exchange_rate)
if frappe.get_cached_value(
"Currency Exchange Settings", "Currency Exchange Settings", "disabled"
):
return 0.00
try: try:
cache = frappe.cache() cache = frappe.cache()
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency)

View File

@ -106,7 +106,6 @@ class TestItem(FrappeTestCase):
"conversion_factor": 1.0, "conversion_factor": 1.0,
"reserved_qty": 1, "reserved_qty": 1,
"actual_qty": 5, "actual_qty": 5,
"ordered_qty": 10,
"projected_qty": 14, "projected_qty": 14,
} }

View File

@ -4,14 +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 typing import Dict, List, Set from typing import Dict, List
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.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.functions import Locate from frappe.query_builder.functions import IfNull, Locate, Sum
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 frappe.utils.nestedset import get_descendants_of
@ -41,7 +41,9 @@ class PickList(Document):
) )
def before_submit(self): def before_submit(self):
update_sales_orders = set() self.validate_picked_items()
def validate_picked_items(self):
for item in self.locations: for item in self.locations:
if self.scan_mode and item.picked_qty < item.stock_qty: if self.scan_mode and item.picked_qty < item.stock_qty:
frappe.throw( frappe.throw(
@ -50,17 +52,14 @@ class PickList(Document):
).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom), ).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom),
title=_("Pick List Incomplete"), title=_("Pick List Incomplete"),
) )
elif not self.scan_mode and item.picked_qty == 0:
if not self.scan_mode and item.picked_qty == 0:
# 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
item.picked_qty = item.stock_qty item.picked_qty = item.stock_qty
if item.sales_order_item:
# update the picked_qty in SO Item
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
if not item.serial_no: if not item.serial_no:
frappe.throw( frappe.throw(
_("Row #{0}: {1} does not have any available serial numbers in {2}").format( _("Row #{0}: {1} does not have any available serial numbers in {2}").format(
@ -68,8 +67,8 @@ class PickList(Document):
), ),
title=_("Serial Nos Required"), title=_("Serial Nos Required"),
) )
if len(item.serial_no.split("\n")) == item.picked_qty:
continue if len(item.serial_no.split("\n")) != item.picked_qty:
frappe.throw( frappe.throw(
_( _(
"For item {0} at row {1}, count of serial numbers does not match with the picked quantity" "For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
@ -77,49 +76,87 @@ class PickList(Document):
title=_("Quantity Mismatch"), title=_("Quantity Mismatch"),
) )
def on_submit(self):
self.update_bundle_picked_qty() self.update_bundle_picked_qty()
self.update_sales_order_picking_status(update_sales_orders) self.update_reference_qty()
self.update_sales_order_picking_status()
def before_cancel(self):
"""Deduct picked qty on cancelling pick list"""
updated_sales_orders = set()
for item in self.get("locations"):
if item.sales_order_item:
self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
updated_sales_orders.add(item.sales_order)
def on_cancel(self):
self.update_bundle_picked_qty() self.update_bundle_picked_qty()
self.update_sales_order_picking_status(updated_sales_orders) self.update_reference_qty()
self.update_sales_order_picking_status()
def update_sales_order_item(self, item, picked_qty, item_code): def update_reference_qty(self):
item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item" packed_items = []
stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty" so_items = []
already_picked, actual_qty = frappe.db.get_value( for item in self.locations:
item_table, if item.product_bundle_item:
item.sales_order_item, packed_items.append(item.sales_order_item)
["picked_qty", stock_qty_field], elif item.sales_order_item:
for_update=True, so_items.append(item.sales_order_item)
if packed_items:
self.update_packed_items_qty(packed_items)
if so_items:
self.update_sales_order_item_qty(so_items)
def update_packed_items_qty(self, packed_items):
picked_items = get_picked_items_qty(packed_items)
self.validate_picked_qty(picked_items)
picked_qty = frappe._dict()
for d in picked_items:
picked_qty[d.sales_order_item] = d.picked_qty
for packed_item in packed_items:
frappe.db.set_value(
"Packed Item",
packed_item,
"picked_qty",
flt(picked_qty.get(packed_item)),
update_modified=False,
) )
if self.docstatus == 1: def update_sales_order_item_qty(self, so_items):
if (((already_picked + picked_qty) / actual_qty) * 100) > ( picked_items = get_picked_items_qty(so_items)
100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")) self.validate_picked_qty(picked_items)
):
picked_qty = frappe._dict()
for d in picked_items:
picked_qty[d.sales_order_item] = d.picked_qty
for so_item in so_items:
frappe.db.set_value(
"Sales Order Item",
so_item,
"picked_qty",
flt(picked_qty.get(so_item)),
update_modified=False,
)
def update_sales_order_picking_status(self) -> None:
sales_orders = []
for row in self.locations:
if row.sales_order and row.sales_order not in sales_orders:
sales_orders.append(row.sales_order)
for sales_order in sales_orders:
frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status()
def validate_picked_qty(self, data):
over_delivery_receipt_allowance = 100 + flt(
frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")
)
for row in data:
if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance:
frappe.throw( frappe.throw(
_( _(
"You are picking more than required quantity for {}. Check if there is any other pick list created for {}" f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}."
).format(item_code, item.sales_order) )
) )
frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty)
@staticmethod
def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
for sales_order in sales_orders:
if sales_order:
frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status()
@frappe.whitelist() @frappe.whitelist()
def set_item_locations(self, save=False): def set_item_locations(self, save=False):
@ -309,6 +346,31 @@ class PickList(Document):
return int(flt(min(possible_bundles), precision or 6)) return int(flt(min(possible_bundles), precision or 6))
def get_picked_items_qty(items) -> List[Dict]:
return frappe.db.sql(
f"""
SELECT
sales_order_item,
item_code,
sales_order,
SUM(stock_qty) AS stock_qty,
SUM(picked_qty) AS picked_qty
FROM
`tabPick List Item`
WHERE
sales_order_item IN (
{", ".join(frappe.db.escape(d) for d in items)}
)
AND docstatus = 1
GROUP BY
sales_order_item,
sales_order
FOR UPDATE
""",
as_dict=1,
)
def validate_item_locations(pick_list): def validate_item_locations(pick_list):
if not pick_list.locations: if not pick_list.locations:
frappe.throw(_("Add items in the Item Locations table")) frappe.throw(_("Add items in the Item Locations table"))
@ -441,42 +503,30 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item( def get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company
): ):
warehouse_condition = "and warehouse in %(warehouses)s" if from_warehouses else "" sle = frappe.qb.DocType("Stock Ledger Entry")
batch_locations = frappe.db.sql( batch = frappe.qb.DocType("Batch")
"""
SELECT query = (
sle.`warehouse`, frappe.qb.from_(sle)
sle.`batch_no`, .from_(batch)
SUM(sle.`actual_qty`) AS `qty` .select(sle.warehouse, sle.batch_no, Sum(sle.actual_qty).as_("qty"))
FROM .where(
`tabStock Ledger Entry` sle, `tabBatch` batch (sle.batch_no == batch.name)
WHERE & (sle.item_code == item_code)
sle.batch_no = batch.name & (sle.company == company)
and sle.`item_code`=%(item_code)s & (batch.disabled == 0)
and sle.`company` = %(company)s & (sle.is_cancelled == 0)
and batch.disabled = 0 & (IfNull(batch.expiry_date, "2200-01-01") > today())
and sle.is_cancelled=0 )
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s .groupby(sle.warehouse, sle.batch_no, sle.item_code)
{warehouse_condition} .having(Sum(sle.actual_qty) > 0)
GROUP BY .orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
sle.`warehouse`,
sle.`batch_no`,
sle.`item_code`
HAVING `qty` > 0
ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`, sle.`batch_no`, sle.`warehouse`
""".format(
warehouse_condition=warehouse_condition
),
{ # nosec
"item_code": item_code,
"company": company,
"today": today(),
"warehouses": from_warehouses,
},
as_dict=1,
) )
return batch_locations if from_warehouses:
query = query.where(sle.warehouse.isin(from_warehouses))
return query.run(as_dict=True)
def get_available_item_locations_for_serial_and_batched_item( def get_available_item_locations_for_serial_and_batched_item(

View File

@ -1,7 +1,10 @@
def get_data(): def get_data():
return { return {
"fieldname": "pick_list", "fieldname": "pick_list",
"internal_links": {
"Sales Order": ["locations", "sales_order"],
},
"transactions": [ "transactions": [
{"items": ["Stock Entry", "Delivery Note"]}, {"items": ["Stock Entry", "Sales Order", "Delivery Note"]},
], ],
} }

View File

@ -236,8 +236,10 @@ def validate_item_details(args, item):
validate_end_of_life(item.name, item.end_of_life, item.disabled) validate_end_of_life(item.name, item.end_of_life, item.disabled)
if args.transaction_type == "selling" and cint(item.has_variants): if cint(item.has_variants):
throw(_("Item {0} is a template, please select one of its variants").format(item.name)) msg = f"Item {item.name} is a template, please select one of its variants"
throw(_(msg), title=_("Template Item Selected"))
elif args.transaction_type == "buying" and args.doctype != "Material Request": elif args.transaction_type == "buying" and args.doctype != "Material Request":
if args.get("is_subcontracted"): if args.get("is_subcontracted"):
@ -1181,7 +1183,7 @@ def get_projected_qty(item_code, warehouse):
@frappe.whitelist() @frappe.whitelist()
def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False): def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False):
bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0} bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
if warehouse: if warehouse:
from frappe.query_builder.functions import Coalesce, Sum from frappe.query_builder.functions import Coalesce, Sum
@ -1197,7 +1199,6 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses
Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"), Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"),
Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"), Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"),
Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"), Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"),
Coalesce(Sum(bin.ordered_qty), 0).as_("ordered_qty"),
) )
.where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses))) .where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses)))
).run(as_dict=True)[0] ).run(as_dict=True)[0]

View File

@ -262,13 +262,15 @@ class SubcontractingReceipt(SubcontractingController):
def get_gl_entries(self, warehouse_account=None): def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
if not erpnext.is_perpetual_inventory_enabled(self.company):
return []
gl_entries = [] gl_entries = []
self.make_item_gl_entries(gl_entries, warehouse_account) self.make_item_gl_entries(gl_entries, warehouse_account)
return process_gl_map(gl_entries) return process_gl_map(gl_entries)
def make_item_gl_entries(self, gl_entries, warehouse_account=None): def make_item_gl_entries(self, gl_entries, warehouse_account=None):
if erpnext.is_perpetual_inventory_enabled(self.company):
stock_rbnb = self.get_company_default("stock_received_but_not_billed") stock_rbnb = self.get_company_default("stock_received_but_not_billed")
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")