Merge branch 'frappe:develop' into develop

This commit is contained in:
Vladislav 2022-06-15 15:03:19 +03:00 committed by GitHub
commit 041a5e7d1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 850 additions and 561 deletions

View File

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

View File

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

View File

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

View File

@ -1239,7 +1239,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
AND jv.docstatus = 1 AND jv.docstatus = 1
AND jv.`{0}` LIKE %(txt)s AND jv.`{0}` LIKE %(txt)s
ORDER BY jv.name DESC ORDER BY jv.name DESC
LIMIT %(offset)s, %(limit)s LIMIT %(limit)s offset %(offset)s
""".format( """.format(
searchfield searchfield
), ),

View File

@ -1444,7 +1444,7 @@ def get_negative_outstanding_invoices(
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = "" supplier_condition = ""
if voucher_type == "Purchase Invoice": if voucher_type == "Purchase Invoice":
supplier_condition = "and (release_date is null or release_date <= CURDATE())" supplier_condition = "and (release_date is null or release_date <= CURRENT_DATE)"
if party_account_currency == company_currency: if party_account_currency == company_currency:
grand_total_field = "base_grand_total" grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total" rounded_total_field = "base_rounded_total"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,14 +2,18 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import itertools
from json import loads from json import loads
from typing import List, Tuple from typing import TYPE_CHECKING, List, Optional, Tuple
import frappe import frappe
import frappe.defaults import frappe.defaults
from frappe import _, qb, throw from frappe import _, qb, throw
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.query_builder.utils import DocType
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
from pypika import Order
from pypika.terms import ExistsCriterion
import erpnext import erpnext
@ -19,6 +23,9 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on from erpnext.stock.utils import get_stock_value_on
if TYPE_CHECKING:
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
class FiscalYearError(frappe.ValidationError): class FiscalYearError(frappe.ValidationError):
pass pass
@ -28,6 +35,9 @@ class PaymentEntryUnlinkError(frappe.ValidationError):
pass pass
GL_REPOSTING_CHUNK = 100
@frappe.whitelist() @frappe.whitelist()
def get_fiscal_year( def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
@ -42,37 +52,32 @@ def get_fiscal_years(
if not fiscal_years: if not fiscal_years:
# if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate) # if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate)
cond = "" FY = DocType("Fiscal Year")
if fiscal_year:
cond += " and fy.name = {0}".format(frappe.db.escape(fiscal_year))
if company:
cond += """
and (not exists (select name
from `tabFiscal Year Company` fyc
where fyc.parent = fy.name)
or exists(select company
from `tabFiscal Year Company` fyc
where fyc.parent = fy.name
and fyc.company=%(company)s)
)
"""
fiscal_years = frappe.db.sql( query = (
""" frappe.qb.from_(FY)
select .select(FY.name, FY.year_start_date, FY.year_end_date)
fy.name, fy.year_start_date, fy.year_end_date .where(FY.disabled == 0)
from
`tabFiscal Year` fy
where
disabled = 0 {0}
order by
fy.year_start_date desc""".format(
cond
),
{"company": company},
as_dict=True,
) )
if fiscal_year:
query = query.where(FY.name == fiscal_year)
if company:
FYC = DocType("Fiscal Year Company")
query = query.where(
ExistsCriterion(frappe.qb.from_(FYC).select(FYC.name).where(FYC.parent == FY.name)).negate()
| ExistsCriterion(
frappe.qb.from_(FYC)
.select(FYC.company)
.where(FYC.parent == FY.name)
.where(FYC.company == company)
)
)
query = query.orderby(FY.year_start_date, Order.desc)
fiscal_years = query.run(as_dict=True)
frappe.cache().hset("fiscal_years", company, fiscal_years) frappe.cache().hset("fiscal_years", company, fiscal_years)
if not transaction_date and not fiscal_year: if not transaction_date and not fiscal_year:
@ -1122,7 +1127,11 @@ def update_gl_entries_after(
def repost_gle_for_stock_vouchers( def repost_gle_for_stock_vouchers(
stock_vouchers, posting_date, company=None, warehouse_account=None stock_vouchers: List[Tuple[str, str]],
posting_date: str,
company: Optional[str] = None,
warehouse_account=None,
repost_doc: Optional["RepostItemValuation"] = None,
): ):
from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative
@ -1130,40 +1139,50 @@ def repost_gle_for_stock_vouchers(
if not stock_vouchers: if not stock_vouchers:
return return
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if repost_doc and repost_doc.gl_reposting_index:
# Restore progress
stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2 precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) stock_vouchers_iterator = iter(stock_vouchers)
for idx, (voucher_type, voucher_no) in enumerate(stock_vouchers):
existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
# Some transactions post credit as negative debit, this is handled while posting GLE
# but while comparing we need to make sure it's flipped so comparisons are accurate
expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision
):
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no)
if idx % 20 == 0: while stock_vouchers_chunk := list(itertools.islice(stock_vouchers_iterator, GL_REPOSTING_CHUNK)):
# Commit every 20 documents to avoid losing progress gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date)
# and reducing memory usage
frappe.db.commit() for voucher_type, voucher_no in stock_vouchers_chunk:
existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
# Some transactions post credit as negative debit, this is handled while posting GLE
# but while comparing we need to make sure it's flipped so comparisons are accurate
expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision
):
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no)
frappe.db.commit()
if repost_doc:
repost_doc.db_set(
"gl_reposting_index",
cint(repost_doc.gl_reposting_index) + GL_REPOSTING_CHUNK,
)
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
def sort_stock_vouchers_by_posting_date( def sort_stock_vouchers_by_posting_date(
@ -1177,6 +1196,9 @@ def sort_stock_vouchers_by_posting_date(
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation) .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no) .groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_date)
.orderby(sle.posting_time)
.orderby(sle.creation)
).run(as_dict=True) ).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
idx desc, idx desc,
name, employee_name name, employee_name
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
**{ **{
"fields": ", ".join(fields), "fields": ", ".join(fields),
"key": searchfield, "key": searchfield,
@ -65,7 +65,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999), if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999),
idx desc, idx desc,
name, lead_name name, lead_name
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
**{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} **{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
), ),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
@ -100,7 +100,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999), if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999),
idx desc, idx desc,
name, customer_name name, customer_name
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
**{ **{
"fields": ", ".join(fields), "fields": ", ".join(fields),
"scond": searchfields, "scond": searchfields,
@ -137,7 +137,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999), if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
idx desc, idx desc,
name, supplier_name name, supplier_name
limit %(start)s, %(page_len)s """.format( limit %(page_len)s offset %(start)s""".format(
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
), ),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
@ -167,7 +167,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
AND `{searchfield}` LIKE %(txt)s AND `{searchfield}` LIKE %(txt)s
{mcond} {mcond}
ORDER BY idx DESC, name ORDER BY idx DESC, name
LIMIT %(offset)s, %(limit)s LIMIT %(limit)s offset %(offset)s
""".format( """.format(
account_type_condition=account_type_condition, account_type_condition=account_type_condition,
searchfield=searchfield, searchfield=searchfield,
@ -351,7 +351,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc, idx desc,
`tabProject`.name asc `tabProject`.name asc
limit {start}, {page_len}""".format( limit {page_len} offset {start}""".format(
fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]), fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
cond=cond, cond=cond,
scond=searchfields, scond=searchfields,
@ -383,7 +383,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
and return_against in (select name from `tabDelivery Note` where per_billed < 100) and return_against in (select name from `tabDelivery Note` where per_billed < 100)
) )
) )
%(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s %(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(page_len)s offset %(start)s
""" """
% { % {
"fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]), "fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]),
@ -456,7 +456,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
{match_conditions} {match_conditions}
group by batch_no {having_clause} group by batch_no {having_clause}
order by batch.expiry_date, sle.batch_no desc order by batch.expiry_date, sle.batch_no desc
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
search_columns=search_columns, search_columns=search_columns,
cond=cond, cond=cond,
match_conditions=get_match_cond(doctype), match_conditions=get_match_cond(doctype),
@ -483,7 +483,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
{match_conditions} {match_conditions}
order by expiry_date, name desc order by expiry_date, name desc
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
cond, cond,
search_columns=search_columns, search_columns=search_columns,
search_cond=search_cond, search_cond=search_cond,
@ -662,7 +662,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
{fcond} {mcond} {fcond} {mcond}
order by ifnull(`tabBin`.actual_qty, 0) desc order by ifnull(`tabBin`.actual_qty, 0) desc
limit limit
{start}, {page_len} {page_len} offset {start}
""".format( """.format(
bin_conditions=get_filters_cond( bin_conditions=get_filters_cond(
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
@ -691,7 +691,7 @@ def get_doctype_wise_filters(filters):
def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters):
query = """select batch_id from `tabBatch` query = """select batch_id from `tabBatch`
where disabled = 0 where disabled = 0
and (expiry_date >= CURDATE() or expiry_date IS NULL) and (expiry_date >= CURRENT_DATE or expiry_date IS NULL)
and name like {txt}""".format( and name like {txt}""".format(
txt=frappe.db.escape("%{0}%".format(txt)) txt=frappe.db.escape("%{0}%".format(txt))
) )

View File

@ -35,7 +35,8 @@ status_map = {
["Draft", None], ["Draft", None],
["Open", "eval:self.docstatus==1"], ["Open", "eval:self.docstatus==1"],
["Lost", "eval:self.status=='Lost'"], ["Lost", "eval:self.status=='Lost'"],
["Ordered", "has_sales_order"], ["Partially Ordered", "is_partially_ordered"],
["Ordered", "is_fully_ordered"],
["Cancelled", "eval:self.docstatus==2"], ["Cancelled", "eval:self.docstatus==2"],
], ],
"Sales Order": [ "Sales Order": [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,13 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_dimensions, get_dimensions,
make_dimension_in_accounting_doctypes, make_dimension_in_accounting_doctypes,
) )
from erpnext.accounts.utils import create_payment_ledger_entry
def create_accounting_dimension_fields(): def create_accounting_dimension_fields():
@ -15,24 +17,119 @@ def create_accounting_dimension_fields():
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
def execute(): def generate_name_for_payment_ledger_entries(gl_entries):
# create accounting dimension fields in Payment Ledger for index, entry in enumerate(gl_entries, 1):
create_accounting_dimension_fields() entry.name = index
def get_columns():
columns = [
"name",
"creation",
"modified",
"modified_by",
"owner",
"docstatus",
"posting_date",
"account_type",
"account",
"party_type",
"party",
"voucher_type",
"voucher_no",
"against_voucher_type",
"against_voucher_no",
"amount",
"amount_in_account_currency",
"account_currency",
"company",
"cost_center",
"due_date",
"finance_book",
]
dimensions_and_defaults = get_dimensions()
if dimensions_and_defaults:
for dimension in dimensions_and_defaults[0]:
columns.append(dimension.fieldname)
return columns
def build_insert_query():
ple = qb.DocType("Payment Ledger Entry")
columns = get_columns()
insert_query = qb.into(ple)
# build 'insert' columns in query
insert_query = insert_query.columns(tuple(columns))
return insert_query
def insert_chunk_into_payment_ledger(insert_query, gl_entries):
if gl_entries:
columns = get_columns()
# build tuple of data with same column order
for entry in gl_entries:
data = ()
for column in columns:
data += (entry[column],)
insert_query = insert_query.insert(data)
insert_query.run()
def execute():
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
# create accounting dimension fields in Payment Ledger
create_accounting_dimension_fields()
gl = qb.DocType("GL Entry")
account = qb.DocType("Account")
gl = qb.DocType("GL Entry")
accounts = frappe.db.get_list(
"Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True
)
gl_entries = []
if accounts:
# get all gl entries on receivable/payable accounts
gl_entries = ( gl_entries = (
qb.from_(gl) qb.from_(gl)
.select("*") .inner_join(account)
.where(gl.account.isin(accounts)) .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
.select(
gl.star,
ConstantColumn(1).as_("docstatus"),
account.account_type.as_("account_type"),
IfNull(gl.against_voucher_type, gl.voucher_type).as_("against_voucher_type"),
IfNull(gl.against_voucher, gl.voucher_no).as_("against_voucher_no"),
# convert debit/credit to amount
Case()
.when(account.account_type == "Receivable", gl.debit - gl.credit)
.else_(gl.credit - gl.debit)
.as_("amount"),
# convert debit/credit in account currency to amount in account currency
Case()
.when(
account.account_type == "Receivable",
gl.debit_in_account_currency - gl.credit_in_account_currency,
)
.else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
.as_("amount_in_account_currency"),
)
.where(gl.is_cancelled == 0) .where(gl.is_cancelled == 0)
.orderby(gl.creation)
.run(as_dict=True) .run(as_dict=True)
) )
if gl_entries:
# create payment ledger entries for the accounts receivable/payable # primary key(name) for payment ledger records
create_payment_ledger_entry(gl_entries, 0) generate_name_for_payment_ledger_entries(gl_entries)
# split data into chunks
chunk_size = 1000
try:
for i in range(0, len(gl_entries), chunk_size):
insert_query = build_insert_query()
insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size])
frappe.db.commit()
except Exception as err:
frappe.db.rollback()
ple = qb.DocType("Payment Ledger Entry")
qb.from_(ple).delete().where(ple.docstatus >= 0).run()
frappe.db.commit()
raise err

View File

@ -970,7 +970,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
and name not in and name not in
(select reference_name from `tabJournal Entry Account` (select reference_name from `tabJournal Entry Account`
where reference_type="Payroll Entry") where reference_type="Payroll Entry")
order by name limit %(start)s, %(page_len)s""".format( order by name limit %(page_len)s offset %(start)s""".format(
key=searchfield key=searchfield
), ),
{"txt": "%%%s%%" % txt, "start": start, "page_len": page_len}, {"txt": "%%%s%%" % txt, "start": start, "page_len": page_len},
@ -1039,7 +1039,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
idx desc, idx desc,
name, employee_name name, employee_name
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
**{ **{
"key": searchfield, "key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions), "fcond": get_filters_cond(doctype, filters, conditions),

View File

@ -391,7 +391,7 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999), if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999),
idx desc, idx desc,
name, full_name name, full_name
limit %(start)s, %(page_len)s""".format( limit %(page_len)s offset %(start)s""".format(
**{ **{
"key": searchfield, "key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions), "fcond": get_filters_cond(doctype, filters, conditions),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ from frappe.utils.data import fmt_money
from frappe.utils.jinja import render_template from frappe.utils.jinja import render_template
from frappe.utils.pdf import get_pdf from frappe.utils.pdf import get_pdf
from frappe.utils.print_format import read_multi_pdf from frappe.utils.print_format import read_multi_pdf
from PyPDF2 import PdfFileWriter from PyPDF2 import PdfWriter
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
@ -106,7 +106,7 @@ def irs_1099_print(filters):
columns, data = execute(filters) columns, data = execute(filters)
template = frappe.get_doc("Print Format", "IRS 1099 Form").html template = frappe.get_doc("Print Format", "IRS 1099 Form").html
output = PdfFileWriter() output = PdfWriter()
for row in data: for row in data:
row["fiscal_year"] = fiscal_year row["fiscal_year"] = fiscal_year

View File

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

View File

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

View File

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

View File

@ -70,8 +70,32 @@ class Quotation(SellingController):
title=_("Unpublished Item"), title=_("Unpublished Item"),
) )
def has_sales_order(self): def get_ordered_status(self):
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1}) ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
{"prevdoc_docname": self.name, "docstatus": 1},
["item_code", "sum(qty)"],
group_by="item_code",
as_list=1,
)
)
status = "Open"
if ordered_items:
status = "Ordered"
for item in self.get("items"):
if item.qty > ordered_items.get(item.item_code, 0.0):
status = "Partially Ordered"
return status
def is_fully_ordered(self):
return self.get_ordered_status() == "Ordered"
def is_partially_ordered(self):
return self.get_ordered_status() == "Partially Ordered"
def update_lead(self): def update_lead(self):
if self.quotation_to == "Lead" and self.party_name: if self.quotation_to == "Lead" and self.party_name:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -854,7 +854,7 @@ class EmailDigest(Document):
sql_po = """select {fields} from `tabPurchase Order Item` sql_po = """select {fields} from `tabPurchase Order Item`
left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent
where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date
and received_qty < qty order by `tabPurchase Order Item`.parent DESC, and received_qty < qty order by `tabPurchase Order Item`.parent DESC,
`tabPurchase Order Item`.schedule_date DESC""".format( `tabPurchase Order Item`.schedule_date DESC""".format(
fields=fields_po fields=fields_po
@ -862,7 +862,7 @@ class EmailDigest(Document):
sql_poi = """select {fields} from `tabPurchase Order Item` sql_poi = """select {fields} from `tabPurchase Order Item`
left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent
where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date
and received_qty < qty order by `tabPurchase Order Item`.idx""".format( and received_qty < qty order by `tabPurchase Order Item`.idx""".format(
fields=fields_poi fields=fields_poi
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -232,7 +232,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
FROM `tab{doc}` FROM `tab{doc}`
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond} {qi_condition} {cond} {mcond}
ORDER BY item_code limit {start}, {page_len} ORDER BY item_code limit {page_len} offset {start}
""".format( """.format(
doc=filters.get("from"), doc=filters.get("from"),
cond=cond, cond=cond,
@ -252,7 +252,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
{qi_condition} {cond} {mcond} {qi_condition} {cond} {mcond}
ORDER BY production_item ORDER BY production_item
LIMIT {start}, {page_len} limit {page_len} offset {start}
""".format( """.format(
doc=filters.get("from"), doc=filters.get("from"),
cond=cond, cond=cond,

View File

@ -25,7 +25,8 @@
"items_to_be_repost", "items_to_be_repost",
"affected_transactions", "affected_transactions",
"distinct_item_and_warehouse", "distinct_item_and_warehouse",
"current_index" "current_index",
"gl_reposting_index"
], ],
"fields": [ "fields": [
{ {
@ -181,12 +182,20 @@
"label": "Affected Transactions", "label": "Affected Transactions",
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "gl_reposting_index",
"fieldtype": "Int",
"hidden": 1,
"label": "GL reposting index",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-04-18 14:08:08.821602", "modified": "2022-06-13 12:20:22.182322",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Repost Item Valuation", "name": "Repost Item Valuation",

View File

@ -192,6 +192,7 @@ def repost_gl_entries(doc):
directly_dependent_transactions + list(repost_affected_transaction), directly_dependent_transactions + list(repost_affected_transaction),
doc.posting_date, doc.posting_date,
doc.company, doc.company,
repost_doc=doc,
) )

View File

@ -2,10 +2,14 @@
# See license.txt # See license.txt
from unittest.mock import MagicMock, call
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate from frappe.utils import nowdate
from frappe.utils.data import today
from erpnext.accounts.utils import repost_gle_for_stock_vouchers
from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.controllers.stock_controller import create_item_wise_repost_entries
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
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
@ -193,3 +197,31 @@ class TestRepostItemValuation(FrappeTestCase):
[["a", "b"], ["c", "d"]], [["a", "b"], ["c", "d"]],
sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))), sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))),
) )
def test_gl_repost_progress(self):
from erpnext.accounts import utils
# lower numbers to simplify test
orig_chunk_size = utils.GL_REPOSTING_CHUNK
utils.GL_REPOSTING_CHUNK = 1
self.addCleanup(setattr, utils, "GL_REPOSTING_CHUNK", orig_chunk_size)
doc = frappe.new_doc("Repost Item Valuation")
doc.db_set = MagicMock()
vouchers = []
company = "_Test Company with perpetual inventory"
posting_date = today()
for _ in range(3):
se = make_stock_entry(company=company, qty=1, rate=2, target="Stores - TCP1")
vouchers.append((se.doctype, se.name))
repost_gle_for_stock_vouchers(stock_vouchers=vouchers, posting_date=posting_date, repost_doc=doc)
self.assertIn(call("gl_reposting_index", 1), doc.db_set.mock_calls)
doc.db_set.reset_mock()
doc.gl_reposting_index = 1
repost_gle_for_stock_vouchers(stock_vouchers=vouchers, posting_date=posting_date, repost_doc=doc)
self.assertNotIn(call("gl_reposting_index", 1), doc.db_set.mock_calls)

View File

@ -1980,23 +1980,30 @@ class StockEntry(StockController):
): ):
# Get PO Supplied Items Details # Get PO Supplied Items Details
item_wh = frappe._dict( po_supplied_items = frappe.db.get_all(
frappe.db.sql( "Purchase Order Item Supplied",
""" filters={"parent": self.purchase_order},
select rm_item_code, reserve_warehouse fields=["name", "rm_item_code", "reserve_warehouse"],
from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
where po.name = poitemsup.parent
and po.name = %s""",
self.purchase_order,
)
) )
# Get Items Supplied in Stock Entries against PO
supplied_items = get_supplied_items(self.purchase_order) supplied_items = get_supplied_items(self.purchase_order)
for name, item in supplied_items.items():
frappe.db.set_value("Purchase Order Item Supplied", name, item)
# Update reserved sub contracted quantity in bin based on Supplied Item Details and for row in po_supplied_items:
key, item = row.name, {}
if not supplied_items.get(key):
# no stock transferred against PO Supplied Items row
item = {"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}
else:
item = supplied_items.get(key)
frappe.db.set_value("Purchase Order Item Supplied", row.name, item)
# RM Item-Reserve Warehouse Dict
item_wh = {x.get("rm_item_code"): x.get("reserve_warehouse") for x in po_supplied_items}
for d in self.get("items"): for d in self.get("items"):
# Update reserved sub contracted quantity in bin based on Supplied Item Details and
item_code = d.get("original_item") or d.get("item_code") item_code = d.get("original_item") or d.get("item_code")
reserve_warehouse = item_wh.get(item_code) reserve_warehouse = item_wh.get(item_code)
if not (reserve_warehouse and item_code): if not (reserve_warehouse and item_code):

View File

@ -1352,12 +1352,22 @@ def get_price_list_currency_and_exchange_rate(args):
@frappe.whitelist() @frappe.whitelist()
def get_default_bom(item_code=None): def get_default_bom(item_code=None):
if item_code: def _get_bom(item):
bom = frappe.db.get_value( bom = frappe.get_all(
"BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code} "BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1
) )
if bom: return bom[0].name if bom else None
return bom
if not item_code:
return
bom_name = _get_bom(item_code)
template_item = frappe.db.get_value("Item", item_code, "variant_of")
if not bom_name and template_item:
bom_name = _get_bom(template_item)
return bom_name
@frappe.whitelist() @frappe.whitelist()

View File

@ -11,6 +11,8 @@ from frappe.core.utils import get_parent_doc
from frappe.email.inbox import link_communication_to_document from frappe.email.inbox import link_communication_to_document
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Interval
from frappe.query_builder.functions import Now
from frappe.utils import date_diff, get_datetime, now_datetime, time_diff_in_seconds from frappe.utils import date_diff, get_datetime, now_datetime, time_diff_in_seconds
from frappe.utils.user import is_website_user from frappe.utils.user import is_website_user
@ -190,15 +192,17 @@ def auto_close_tickets():
frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7
) )
issues = frappe.db.sql( table = frappe.qb.DocType("Issue")
""" select name from tabIssue where status='Replied' and issues = (
modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """, frappe.qb.from_(table)
(auto_close_after_days), .select(table.name)
as_dict=True, .where(
) (table.modified < (Now() - Interval(days=auto_close_after_days))) & (table.status == "Replied")
)
).run(pluck=True)
for issue in issues: for issue in issues:
doc = frappe.get_doc("Issue", issue.get("name")) doc = frappe.get_doc("Issue", issue)
doc.status = "Closed" doc.status = "Closed"
doc.flags.ignore_permissions = True doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True

View File

@ -56,7 +56,10 @@ def get_product_data(search=None, start=0, limit=12):
search = "%" + cstr(search) + "%" search = "%" + cstr(search) + "%"
# order by # order by
query += """ ORDER BY ranking desc, modified desc limit %s, %s""" % (cint(start), cint(limit)) query += """ ORDER BY ranking desc, modified desc limit %s offset %s""" % (
cint(limit),
cint(start),
)
return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep

View File

@ -59,7 +59,6 @@ class TestExotel(FrappeAPITestCase):
f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}", f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}",
data=frappe.as_json(data), data=frappe.as_json(data),
content_type="application/json", content_type="application/json",
as_tuple=True,
) )
# restart db connection to get latest data # restart db connection to get latest data
frappe.connect() frappe.connect()

View File

@ -879,6 +879,55 @@ class TestSubcontracting(unittest.TestCase):
for key, value in get_supplied_items(pr1).items(): for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 2) self.assertEqual(value.qty, 2)
def test_po_supplied_qty(self):
"""
Check if 'Supplied Qty' in PO's Supplied Items table is reset on submit/cancel.
"""
set_backflush_based_on("Material Transferred for Subcontract")
items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Item SA1",
"qty": 5,
"rate": 100,
},
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Item SA5",
"qty": 6,
"rate": 100,
},
]
rm_items = [
{"item_code": "Subcontracted SRM Item 1", "qty": 5, "main_item_code": "Subcontracted Item SA1"},
{"item_code": "Subcontracted SRM Item 2", "qty": 5, "main_item_code": "Subcontracted Item SA1"},
{"item_code": "Subcontracted SRM Item 3", "qty": 5, "main_item_code": "Subcontracted Item SA1"},
{"item_code": "Subcontracted SRM Item 5", "qty": 6, "main_item_code": "Subcontracted Item SA5"},
{"item_code": "Subcontracted SRM Item 4", "qty": 6, "main_item_code": "Subcontracted Item SA5"},
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(
rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
)
for d in rm_items:
d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name
se = make_stock_transfer_entry(
po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)
)
po.reload()
for row in po.get("supplied_items"):
self.assertIn(row.supplied_qty, [5.0, 6.0])
se.cancel()
po.reload()
for row in po.get("supplied_items"):
self.assertEqual(row.supplied_qty, 0.0)
def add_second_row_in_pr(pr): def add_second_row_in_pr(pr):
item_dict = {} item_dict = {}

View File

@ -1352,11 +1352,11 @@ Item Description,Description de l'Article,
Item Group,Groupe d'Article, Item Group,Groupe d'Article,
Item Group Tree,Arborescence de Groupe d'Article, Item Group Tree,Arborescence de Groupe d'Article,
Item Group not mentioned in item master for item {0},Le Groupe d'Articles n'est pas mentionné dans la fiche de l'article pour l'article {0}, Item Group not mentioned in item master for item {0},Le Groupe d'Articles n'est pas mentionné dans la fiche de l'article pour l'article {0},
Item Name,Nom de l&#39;article, Item Name,Nom de l'article,
Item Price added for {0} in Price List {1},Prix de l'Article ajouté pour {0} dans la Liste de Prix {1}, Item Price added for {0} in Price List {1},Prix de l'Article ajouté pour {0} dans la Liste de Prix {1},
"Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates.","Le prix de l&#39;article apparaît plusieurs fois en fonction de la liste de prix, du fournisseur / client, de la devise, de l&#39;article, de l&#39;unité de mesure, de la quantité et des dates.", "Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates.","Le prix de l'article apparaît plusieurs fois en fonction de la liste de prix, du fournisseur / client, de la devise, de l'article, de l'unité de mesure, de la quantité et des dates.",
Item Price updated for {0} in Price List {1},Prix de l'Article mis à jour pour {0} dans la Liste des Prix {1}, Item Price updated for {0} in Price List {1},Prix de l'Article mis à jour pour {0} dans la Liste des Prix {1},
Item Row {0}: {1} {2} does not exist in above '{1}' table,Ligne d&#39;objet {0}: {1} {2} n&#39;existe pas dans la table &#39;{1}&#39; ci-dessus, Item Row {0}: {1} {2} does not exist in above '{1}' table,Ligne d'objet {0}: {1} {2} n'existe pas dans la table '{1}' ci-dessus,
Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,La Ligne de Taxe d'Article {0} doit indiquer un compte de type Taxes ou Produit ou Charge ou Facturable, Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,La Ligne de Taxe d'Article {0} doit indiquer un compte de type Taxes ou Produit ou Charge ou Facturable,
Item Template,Modèle d'article, Item Template,Modèle d'article,
Item Variant Settings,Paramètres de Variante d'Article, Item Variant Settings,Paramètres de Variante d'Article,
@ -3661,7 +3661,7 @@ Chart,Graphique,
Choose a corresponding payment,Choisissez un paiement correspondant, Choose a corresponding payment,Choisissez un paiement correspondant,
Click on the link below to verify your email and confirm the appointment,Cliquez sur le lien ci-dessous pour vérifier votre email et confirmer le rendez-vous, Click on the link below to verify your email and confirm the appointment,Cliquez sur le lien ci-dessous pour vérifier votre email et confirmer le rendez-vous,
Close,Fermer, Close,Fermer,
Communication,la communication, Communication,Communication,
Compact Item Print,Impression de l'Article Compacté, Compact Item Print,Impression de l'Article Compacté,
Company,Société, Company,Société,
Company of asset {0} and purchase document {1} doesn't matches.,La société de l'actif {0} et le document d'achat {1} ne correspondent pas., Company of asset {0} and purchase document {1} doesn't matches.,La société de l'actif {0} et le document d'achat {1} ne correspondent pas.,
@ -3969,7 +3969,7 @@ Quantity to Manufacture can not be zero for the operation {0},La quantité à fa
Quarterly,Trimestriel, Quarterly,Trimestriel,
Queued,File d'Attente, Queued,File d'Attente,
Quick Entry,Écriture Rapide, Quick Entry,Écriture Rapide,
Quiz {0} does not exist,Le questionnaire {0} n&#39;existe pas, Quiz {0} does not exist,Le questionnaire {0} n'existe pas,
Quotation Amount,Montant du devis, Quotation Amount,Montant du devis,
Rate or Discount is required for the price discount.,Le taux ou la remise est requis pour la remise de prix., Rate or Discount is required for the price discount.,Le taux ou la remise est requis pour la remise de prix.,
Reason,Raison, Reason,Raison,
@ -4071,7 +4071,7 @@ Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and
Stores - {0},Magasins - {0}, Stores - {0},Magasins - {0},
Student with email {0} does not exist,Étudiant avec le courrier électronique {0} n'existe pas, Student with email {0} does not exist,Étudiant avec le courrier électronique {0} n'existe pas,
Submit Review,Poster un commentaire, Submit Review,Poster un commentaire,
Submitted,Soumis, Submitted,Valider,
Supplier Addresses And Contacts,Adresses et contacts des fournisseurs, Supplier Addresses And Contacts,Adresses et contacts des fournisseurs,
Synchronize this account,Synchroniser ce compte, Synchronize this account,Synchroniser ce compte,
Tag,Étiquette, Tag,Étiquette,
@ -9871,8 +9871,42 @@ Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les t
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions
Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries
"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités"
Unit Of Measure (UOM),Unité de mesure (UDM),
Allowed Items,Articles autorisés Allowed Items,Articles autorisés
Party Specific Item,Restriction d'article disponible Party Specific Item,Restriction d'article disponible
Restrict Items Based On,Type de critére de restriction Restrict Items Based On,Type de critére de restriction
Based On Value,critére de restriction Based On Value,critére de restriction
Unit of Measure (UOM),Unité de mesure (UDM),
Unit Of Measure (UOM),Unité de mesure (UDM),
CRM Settings,Paramètres CRM
Do Not Explode,Ne pas décomposer
Quick Access, Accés rapides
{} Available,{} Disponible.s
{} Pending,{} En attente.s
{} To Bill,{} à facturer
{} To Receive,{} A recevoir
{} Active,{} Actif.ve(s)
{} Open,{} Ouvert.e(s)
Incorrect Data Report,Rapport de données incohérentes
Incorrect Serial No Valuation,Valorisation inccorecte par Num. Série / Lots
Incorrect Balance Qty After Transaction,Equilibre des quantités aprés une transaction
Interview Type,Type d'entretien
Interview Round,Cycle d'entretien
Interview,Entretien
Interview Feedback,Retour d'entretien
Journal Energy Point,Historique des points d'énergies
Billing Address Details,Adresse de facturation (détails)
Supplier Address Details,Adresse Fournisseur (détails)
Retail,Commerce
Users,Utilisateurs
Permission Manager,Gestion des permissions
Fetch Timesheet,Récuprer les temps saisis
Get Supplier Group Details,Appliquer les informations depuis le Groupe de fournisseur
Quality Inspection(s),Inspection(s) Qualité
Set Advances and Allocate (FIFO),Affecter les encours au réglement
Apply Putaway Rule,Appliquer la régle de routage d'entrepot
Delete Transactions,Supprimer les transactions
Default Payment Discount Account,Compte par défaut des paiements de remise
Unrealized Profit / Loss Account,Compte de perte
Enable Provisional Accounting For Non Stock Items,Activer la provision pour les articles non stockés
Publish in Website,Publier sur le Site Web
List View,Vue en liste

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

View File

@ -1,46 +0,0 @@
# Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe import _
from frappe.utils.bot import BotParser
class FindItemBot(BotParser):
def get_reply(self):
if self.startswith("where is", "find item", "locate"):
if not frappe.has_permission("Warehouse"):
raise frappe.PermissionError
item = "%{0}%".format(self.strip_words(self.query, "where is", "find item", "locate"))
items = frappe.db.sql(
"""select name from `tabItem` where item_code like %(txt)s
or item_name like %(txt)s or description like %(txt)s""",
dict(txt=item),
)
if items:
out = []
warehouses = frappe.get_all("Warehouse")
for item in items:
found = False
for warehouse in warehouses:
qty = frappe.db.get_value(
"Bin", {"item_code": item[0], "warehouse": warehouse.name}, "actual_qty"
)
if qty:
out.append(
_("{0} units of [{1}](/app/Form/Item/{1}) found in [{2}](/app/Form/Warehouse/{2})").format(
qty, item[0], warehouse.name
)
)
found = True
if not found:
out.append(_("[{0}](/app/Form/Item/{0}) is out of stock").format(item[0]))
return "\n\n".join(out)
else:
return _("Did not find any item called {0}").format(item)