Merge branch 'develop' into v12-lead-address-contact

This commit is contained in:
Rohan 2019-12-06 14:25:30 +05:30 committed by GitHub
commit 05222270e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 3460 additions and 5456 deletions

View File

@ -82,9 +82,7 @@
}, },
"Inventory": { "Inventory": {
"Consigned Stock": { "Consigned Stock": {
"Handling Difference in Inventory": { "Handling Difference in Inventory": {},
"account_type": "Stock Adjustment"
},
"Items Delivered to Customs on temporary Base": {} "Items Delivered to Customs on temporary Base": {}
}, },
"Stock in Hand": { "Stock in Hand": {
@ -190,6 +188,9 @@
}, },
"Expenses Included In Valuation": { "Expenses Included In Valuation": {
"account_type": "Expenses Included In Valuation" "account_type": "Expenses Included In Valuation"
},
"Stock Adjustment": {
"account_type": "Stock Adjustment"
} }
}, },
"Depreciation": { "Depreciation": {

View File

@ -185,7 +185,8 @@ def validate_account_types(accounts):
return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)) return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))
account_types_for_group = ["Bank", "Cash", "Stock"] account_types_for_group = ["Bank", "Cash", "Stock"]
account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] not in ('', 1)] # fix logic bug
account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] == 1]
missing = list(set(account_types_for_group) - set(account_groups)) missing = list(set(account_types_for_group) - set(account_groups))
if missing: if missing:

View File

@ -18,6 +18,7 @@ class CostCenter(NestedSet):
def validate(self): def validate(self):
self.validate_mandatory() self.validate_mandatory()
self.validate_parent_cost_center()
def validate_mandatory(self): def validate_mandatory(self):
if self.cost_center_name != self.company and not self.parent_cost_center: if self.cost_center_name != self.company and not self.parent_cost_center:
@ -25,6 +26,12 @@ class CostCenter(NestedSet):
elif self.cost_center_name == self.company and self.parent_cost_center: elif self.cost_center_name == self.company and self.parent_cost_center:
frappe.throw(_("Root cannot have a parent cost center")) frappe.throw(_("Root cannot have a parent cost center"))
def validate_parent_cost_center(self):
if self.parent_cost_center:
if not frappe.db.get_value('Cost Center', self.parent_cost_center, 'is_group'):
frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format(
frappe.bold(self.parent_cost_center)))
def convert_group_to_ledger(self): def convert_group_to_ledger(self):
if self.check_if_child_exists(): if self.check_if_child_exists():
frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes")) frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))

View File

@ -1,12 +1,26 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
test_records = frappe.get_test_records('Cost Center') test_records = frappe.get_test_records('Cost Center')
class TestCostCenter(unittest.TestCase):
def test_cost_center_creation_against_child_node(self):
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
frappe.get_doc(test_records[1]).insert()
cost_center = frappe.get_doc({
'doctype': 'Cost Center',
'cost_center_name': '_Test Cost Center 3',
'parent_cost_center': '_Test Cost Center 2 - _TC',
'is_group': 0,
'company': '_Test Company'
})
self.assertRaises(frappe.ValidationError, cost_center.save)
def create_cost_center(**args): def create_cost_center(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -350,13 +350,13 @@ def get_amount(ref_doc):
if dt in ["Sales Order", "Purchase Order"]: if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
if dt in ["Sales Invoice", "Purchase Invoice"]: elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency: if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount) grand_total = flt(ref_doc.outstanding_amount)
else: else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
if dt == "Fees": elif dt == "Fees":
grand_total = ref_doc.outstanding_amount grand_total = ref_doc.outstanding_amount
if grand_total > 0 : if grand_total > 0 :

View File

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

View File

@ -330,23 +330,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
frm: cur_frm frm: cur_frm
}) })
}, },
item_code: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
if(row.item_code) {
frappe.call({
method: "erpnext.assets.doctype.asset_category.asset_category.get_asset_category_account",
args: {
"item": row.item_code,
"fieldname": "fixed_asset_account",
"company": frm.doc.company
},
callback: function(r, rt) {
frappe.model.set_value(cdt, cdn, "expense_account", r.message);
}
})
}
}
}); });
cur_frm.script_manager.make(erpnext.accounts.PurchaseInvoice); cur_frm.script_manager.make(erpnext.accounts.PurchaseInvoice);

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-05-22 12:43:10", "creation": "2013-05-22 12:43:10",
"doctype": "DocType", "doctype": "DocType",
@ -507,7 +508,8 @@
"depends_on": "enable_deferred_expense", "depends_on": "enable_deferred_expense",
"fieldname": "service_stop_date", "fieldname": "service_stop_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service Stop Date" "label": "Service Stop Date",
"no_copy": 1
}, },
{ {
"default": "0", "default": "0",
@ -523,13 +525,15 @@
"depends_on": "enable_deferred_expense", "depends_on": "enable_deferred_expense",
"fieldname": "service_start_date", "fieldname": "service_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service Start Date" "label": "Service Start Date",
"no_copy": 1
}, },
{ {
"depends_on": "enable_deferred_expense", "depends_on": "enable_deferred_expense",
"fieldname": "service_end_date", "fieldname": "service_end_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service End Date" "label": "Service End Date",
"no_copy": 1
}, },
{ {
"fieldname": "reference", "fieldname": "reference",
@ -766,7 +770,8 @@
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-11-21 16:27:52.043744", "links": [],
"modified": "2019-12-04 12:23:17.046413",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -789,22 +789,21 @@ frappe.ui.form.on('Sales Invoice', {
method: "frappe.client.get_value", method: "frappe.client.get_value",
args:{ args:{
doctype: "Patient", doctype: "Patient",
filters: {"name": frm.doc.patient}, filters: {
"name": frm.doc.patient
},
fieldname: "customer" fieldname: "customer"
}, },
callback:function(patient_customer) { callback:function(r) {
if(patient_customer){ if(r && r.message.customer){
frm.set_value("customer", patient_customer.message.customer); frm.set_value("customer", r.message.customer);
frm.refresh_fields();
} }
} }
}); });
} }
else{
frm.set_value("customer", '');
}
} }
}, },
refresh: function(frm) { refresh: function(frm) {
if (frappe.boot.active_domains.includes("Healthcare")){ if (frappe.boot.active_domains.includes("Healthcare")){
frm.set_df_property("patient", "hidden", 0); frm.set_df_property("patient", "hidden", 0);

View File

@ -538,7 +538,7 @@ class SalesInvoice(SellingController):
is_stock_item = frappe.get_cached_value('Item', d.item_code, 'is_stock_item') is_stock_item = frappe.get_cached_value('Item', d.item_code, 'is_stock_item')
if (d.item_code and is_stock_item == 1\ if (d.item_code and is_stock_item == 1\
and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])): and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])):
msgprint(_("{0} is mandatory for Item {1}").format(i,d.item_code), raise_exception=1) msgprint(_("{0} is mandatory for Stock Item {1}").format(i,d.item_code), raise_exception=1)
def validate_proj_cust(self): def validate_proj_cust(self):
@ -1048,13 +1048,18 @@ class SalesInvoice(SellingController):
continue continue
for serial_no in item.serial_no.split("\n"): for serial_no in item.serial_no.split("\n"):
sales_invoice, item_code = frappe.db.get_value("Serial No", serial_no, serial_no_details = frappe.db.get_value("Serial No", serial_no,
["sales_invoice", "item_code"]) ["sales_invoice", "item_code"], as_dict=1)
if sales_invoice and item_code == item.item_code and self.name != sales_invoice:
sales_invoice_company = frappe.db.get_value("Sales Invoice", sales_invoice, "company") if not serial_no_details:
continue
if serial_no_details.sales_invoice and serial_no_details.item_code == item.item_code \
and self.name != serial_no_details.sales_invoice:
sales_invoice_company = frappe.db.get_value("Sales Invoice", serial_no_details.sales_invoice, "company")
if sales_invoice_company == self.company: if sales_invoice_company == self.company:
frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}" frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}"
.format(serial_no, sales_invoice))) .format(serial_no, serial_no_details.sales_invoice)))
def update_project(self): def update_project(self):
if self.project: if self.project:

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-06-04 11:02:19", "creation": "2013-06-04 11:02:19",
"doctype": "DocType", "doctype": "DocType",
@ -484,7 +485,8 @@
"depends_on": "enable_deferred_revenue", "depends_on": "enable_deferred_revenue",
"fieldname": "service_stop_date", "fieldname": "service_stop_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service Stop Date" "label": "Service Stop Date",
"no_copy": 1
}, },
{ {
"default": "0", "default": "0",
@ -500,13 +502,15 @@
"depends_on": "enable_deferred_revenue", "depends_on": "enable_deferred_revenue",
"fieldname": "service_start_date", "fieldname": "service_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service Start Date" "label": "Service Start Date",
"no_copy": 1
}, },
{ {
"depends_on": "enable_deferred_revenue", "depends_on": "enable_deferred_revenue",
"fieldname": "service_end_date", "fieldname": "service_end_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Service End Date" "label": "Service End Date",
"no_copy": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@ -783,7 +787,8 @@
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-07-16 16:36:46.527606", "links": [],
"modified": "2019-12-04 12:22:38.517710",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -70,7 +70,7 @@ class ShippingRule(Document):
def get_shipping_amount_from_rules(self, value): def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"): for condition in self.get("conditions"):
if not condition.to_value or (flt(condition.from_value) <= value <= flt(condition.to_value)): if not condition.to_value or (flt(condition.from_value) <= flt(value) <= flt(condition.to_value)):
return condition.shipping_amount return condition.shipping_amount
return 0.0 return 0.0

View File

@ -162,33 +162,34 @@ def validate_account_for_perpetual_inventory(gl_map):
frappe.throw(_("Account: {0} can only be updated via Stock Transactions") frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
.format(account), StockAccountInvalidTransaction) .format(account), StockAccountInvalidTransaction)
elif account_bal != stock_bal: # This has been comment for a temporary, will add this code again on release of immutable ledger
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), # elif account_bal != stock_bal:
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) # precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
# currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
diff = flt(stock_bal - account_bal, precision) # diff = flt(stock_bal - account_bal, precision)
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format( # error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format(
stock_bal, account_bal, frappe.bold(account)) # stock_bal, account_bal, frappe.bold(account))
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) # error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") # stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency') # db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency')
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency') # db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency')
journal_entry_args = { # journal_entry_args = {
'accounts':[ # 'accounts':[
{'account': account, db_or_cr_warehouse_account : abs(diff)}, # {'account': account, db_or_cr_warehouse_account : abs(diff)},
{'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }] # {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }]
} # }
frappe.msgprint(msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution), # frappe.msgprint(msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
raise_exception=StockValueAndAccountBalanceOutOfSync, # raise_exception=StockValueAndAccountBalanceOutOfSync,
title=_('Values Out Of Sync'), # title=_('Values Out Of Sync'),
primary_action={ # primary_action={
'label': _('Make Journal Entry'), # 'label': _('Make Journal Entry'),
'client_action': 'erpnext.route_to_adjustment_jv', # 'client_action': 'erpnext.route_to_adjustment_jv',
'args': journal_entry_args # 'args': journal_entry_args
}) # })
def validate_cwip_accounts(gl_map): def validate_cwip_accounts(gl_map):
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])

View File

@ -100,6 +100,11 @@ frappe.query_reports["Accounts Payable"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Supplier Group" "options": "Supplier Group"
}, },
{
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{ {
"fieldname":"tax_id", "fieldname":"tax_id",
"label": __("Tax Id"), "label": __("Tax Id"),

View File

@ -88,6 +88,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
"label": __("Supplier Group"), "label": __("Supplier Group"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Supplier Group" "options": "Supplier Group"
},
{
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
} }
], ],

View File

@ -60,6 +60,7 @@ class ReceivablePayableReport(object):
def get_data(self): def get_data(self):
self.get_gl_entries() self.get_gl_entries()
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
@ -103,12 +104,18 @@ class ReceivablePayableReport(object):
def get_invoices(self, gle): def get_invoices(self, gle):
if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'):
self.invoices.add(gle.voucher_no) if self.filters.get("sales_person"):
if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \
or gle.party in self.sales_person_records.get("Customer", []):
self.invoices.add(gle.voucher_no)
else:
self.invoices.add(gle.voucher_no)
def update_voucher_balance(self, gle): def update_voucher_balance(self, gle):
# 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(gle)
if not row: return
# gle_balance will be the total "debit - credit" for receivable type reports and # gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports # and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle) gle_balance = self.get_gle_balance(gle)
@ -129,8 +136,13 @@ class ReceivablePayableReport(object):
row.paid -= gle_balance row.paid -= gle_balance
def get_voucher_balance(self, gle): def get_voucher_balance(self, gle):
voucher_balance = None 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: if gle.against_voucher:
# find invoice # find invoice
against_voucher = gle.against_voucher against_voucher = gle.against_voucher
@ -318,7 +330,7 @@ class ReceivablePayableReport(object):
self.append_payment_term(row, d, term) self.append_payment_term(row, d, term)
def append_payment_term(self, row, d, term): def append_payment_term(self, row, d, term):
if self.filters.get("customer") and d.currency == d.party_account_currency: if (self.filters.get("customer") or self.filters.get("supplier")) and d.currency == d.party_account_currency:
invoiced = d.payment_amount invoiced = d.payment_amount
else: else:
invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision)
@ -512,6 +524,22 @@ class ReceivablePayableReport(object):
order by posting_date, party""" order by posting_date, party"""
.format(select_fields, conditions), values, as_dict=True) .format(select_fields, conditions), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
lft, rgt = frappe.db.get_value("Sales Person",
self.filters.get("sales_person"), ["lft", "rgt"])
records = frappe.db.sql("""
select distinct parent, parenttype
from `tabSales Team` steam
where parenttype in ('Customer', 'Sales Invoice')
and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
""", (lft, rgt), as_dict=1)
self.sales_person_records = frappe._dict()
for d in records:
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
def prepare_conditions(self): def prepare_conditions(self):
conditions = [""] conditions = [""]
values = [self.party_type, self.filters.report_date] values = [self.party_type, self.filters.report_date]
@ -564,16 +592,6 @@ class ReceivablePayableReport(object):
conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)") conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)")
values.append(self.filters.get("sales_partner")) values.append(self.filters.get("sales_partner"))
if self.filters.get("sales_person"):
lft, rgt = frappe.db.get_value("Sales Person",
self.filters.get("sales_person"), ["lft", "rgt"])
conditions.append("""exists(select name from `tabSales Team` steam where
steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1})
and ((steam.parent = voucher_no and steam.parenttype = voucher_type)
or (steam.parent = against_voucher and steam.parenttype = against_voucher_type)
or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt))
def add_supplier_filters(self, conditions, values): def add_supplier_filters(self, conditions, values):
if self.filters.get("supplier_group"): if self.filters.get("supplier_group"):
conditions.append("""party in (select name from tabSupplier conditions.append("""party in (select name from tabSupplier

View File

@ -106,6 +106,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
"label": __("Sales Person"), "label": __("Sales Person"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Sales Person" "options": "Sales Person"
},
{
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
} }
], ],

View File

@ -36,7 +36,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.filters.report_date) or {} self.filters.report_date) or {}
for party, party_dict in iteritems(self.party_total): for party, party_dict in iteritems(self.party_total):
if party_dict.outstanding <= 0: if party_dict.outstanding == 0:
continue continue
row = frappe._dict() row = frappe._dict()

View File

@ -18,11 +18,14 @@ def execute(filters=None):
return columns, data return columns, data
def get_data(filters, show_party_name): def get_data(filters, show_party_name):
party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) if filters.get('party_type') in ('Customer', 'Supplier', 'Employee', 'Member'):
party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type')))
if filters.get('party_type') == 'Student': if filters.get('party_type') == 'Student':
party_name_field = 'first_name' party_name_field = 'first_name'
elif filters.get('party_type') == 'Shareholder': elif filters.get('party_type') == 'Shareholder':
party_name_field = 'title' party_name_field = 'title'
else:
party_name_field = 'name'
party_filters = {"name": filters.get("party")} if filters.get("party") else {} party_filters = {"name": filters.get("party")} if filters.get("party") else {}
parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field], parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field],

View File

@ -569,7 +569,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
account_balance = get_balance_on(account, posting_date, in_account_currency=False) account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True)
related_warehouses = [wh for wh, wh_details in warehouse_account.items() related_warehouses = [wh for wh, wh_details in warehouse_account.items()
if wh_details.account == account and not wh_details.is_group] if wh_details.account == account and not wh_details.is_group]

View File

@ -517,15 +517,18 @@ def update_maintenance_status():
asset.set_status('Out of Order') asset.set_status('Out of Order')
def make_post_gl_entry(): def make_post_gl_entry():
if not is_cwip_accounting_enabled(self.asset_category):
return
assets = frappe.db.sql_list(""" select name from `tabAsset` asset_categories = frappe.db.get_all('Asset Category', fields = ['name', 'enable_cwip_accounting'])
where ifnull(booked_fixed_asset, 0) = 0 and available_for_use_date = %s""", nowdate())
for asset in assets: for asset_category in asset_categories:
doc = frappe.get_doc('Asset', asset) if cint(asset_category.enable_cwip_accounting):
doc.make_gl_entries() assets = frappe.db.sql_list(""" select name from `tabAsset`
where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0
and available_for_use_date = %s""", (asset_category.name, nowdate()))
for asset in assets:
doc = frappe.get_doc('Asset', asset)
doc.make_gl_entries()
def get_asset_naming_series(): def get_asset_naming_series():
meta = frappe.get_meta('Asset') meta = frappe.get_meta('Asset')

View File

@ -29,7 +29,8 @@ def get_asset_category_account(fieldname, item=None, asset=None, account=None, a
account=None account=None
if not account: if not account:
asset_category, company = frappe.db.get_value("Asset", asset, ["asset_category", "company"]) asset_details = frappe.db.get_value("Asset", asset, ["asset_category", "company"])
asset_category, company = asset_details or [None, None]
account = frappe.db.get_value("Asset Category Account", account = frappe.db.get_value("Asset Category Account",
filters={"parent": asset_category, "company_name": company}, fieldname=fieldname) filters={"parent": asset_category, "company_name": company}, fieldname=fieldname)

View File

@ -26,5 +26,11 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldtype: "Link", fieldtype: "Link",
options: "Finance Book" options: "Finance Book"
}, },
{
fieldname:"date",
label: __("Date"),
fieldtype: "Date",
default: frappe.datetime.get_today()
},
] ]
}; };

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr from frappe.utils import cstr, today, flt
def execute(filters=None): def execute(filters=None):
filters = frappe._dict(filters or {}) filters = frappe._dict(filters or {})
@ -86,8 +86,8 @@ def get_columns(filters):
"width": 90 "width": 90
}, },
{ {
"label": _("Current Value"), "label": _("Asset Value"),
"fieldname": "current_value", "fieldname": "asset_value",
"options": "Currency", "options": "Currency",
"width": 90 "width": 90
}, },
@ -114,7 +114,7 @@ def get_data(filters):
data = [] data = []
conditions = get_conditions(filters) conditions = get_conditions(filters)
current_value_map = get_finance_book_value_map(filters.finance_book) depreciation_amount_map = get_finance_book_value_map(filters.date, filters.finance_book)
pr_supplier_map = get_purchase_receipt_supplier_map() pr_supplier_map = get_purchase_receipt_supplier_map()
pi_supplier_map = get_purchase_invoice_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map()
@ -125,7 +125,9 @@ def get_data(filters):
"available_for_use_date", "status", "purchase_invoice"]) "available_for_use_date", "status", "purchase_invoice"])
for asset in assets_record: for asset in assets_record:
if current_value_map.get(asset.name) is not None: asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \
- flt(depreciation_amount_map.get(asset.name))
if asset_value:
row = { row = {
"asset_id": asset.name, "asset_id": asset.name,
"asset_name": asset.asset_name, "asset_name": asset.asset_name,
@ -138,19 +140,24 @@ def get_data(filters):
"location": asset.location, "location": asset.location,
"asset_category": asset.asset_category, "asset_category": asset.asset_category,
"purchase_date": asset.purchase_date, "purchase_date": asset.purchase_date,
"current_value": current_value_map.get(asset.name) "asset_value": asset_value
} }
data.append(row) data.append(row)
return data return data
def get_finance_book_value_map(finance_book=''): def get_finance_book_value_map(date, finance_book=''):
if not date:
date = today()
return frappe._dict(frappe.db.sql(''' Select return frappe._dict(frappe.db.sql(''' Select
parent, value_after_depreciation parent, SUM(depreciation_amount)
FROM `tabAsset Finance Book` FROM `tabDepreciation Schedule`
WHERE WHERE
parentfield='finance_books' parentfield='schedules'
AND ifnull(finance_book, '')=%s''', cstr(finance_book))) AND schedule_date<=%s
AND journal_entry IS NOT NULL
AND ifnull(finance_book, '')=%s
GROUP BY parent''', (date, cstr(finance_book))))
def get_purchase_receipt_supplier_map(): def get_purchase_receipt_supplier_map():
return frappe._dict(frappe.db.sql(''' Select return frappe._dict(frappe.db.sql(''' Select

View File

@ -18,6 +18,7 @@ frappe.ui.form.on("Purchase Order", {
return { return {
filters: { filters: {
"company": frm.doc.company, "company": frm.doc.company,
"name": ['!=', frm.doc.supplier_warehouse],
"is_group": 0 "is_group": 0
} }
} }
@ -283,6 +284,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
}) })
} }
me.dialog.get_field('sub_con_rm_items').check_all_rows()
me.dialog.show() me.dialog.show()
this.dialog.set_primary_action(__('Transfer'), function() { this.dialog.set_primary_action(__('Transfer'), function() {
me.values = me.dialog.get_values(); me.values = me.dialog.get_values();

View File

@ -17,6 +17,8 @@ from erpnext.stock.doctype.material_request.material_request import make_purchas
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.controllers.status_updater import OverAllowanceError from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
class TestPurchaseOrder(unittest.TestCase): class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self): def test_make_purchase_receipt(self):
@ -519,47 +521,62 @@ class TestPurchaseOrder(unittest.TestCase):
def test_backflush_based_on_stock_entry(self): def test_backflush_based_on_stock_entry(self):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code) make_subcontracted_item(item_code)
make_item('Sub Contracted Raw Material 1', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})
update_backflush_based_on("Material Transferred for Subcontract") update_backflush_based_on("Material Transferred for Subcontract")
po = create_purchase_order(item_code=item_code, qty=1,
order_qty = 5
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",
item_code="_Test Item Home Desktop 100", qty=10, basic_rate=100) item_code="_Test Item Home Desktop 100", qty=10, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=100, basic_rate=100) item_code = "Test Extra Item 1", qty=100, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 2", qty=10, basic_rate=100) item_code = "Test Extra Item 2", qty=10, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 1", qty=10, basic_rate=100)
rm_item = [ rm_items = [
{"item_code":item_code,"rm_item_code":"_Test Item","item_name":"_Test Item", {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 1","item_name":"_Test Item",
"qty":1,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":100,"stock_uom":"Nos"}, "qty":10,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
{"item_code":item_code,"rm_item_code":"_Test Item Home Desktop 100","item_name":"_Test Item Home Desktop 100", {"item_code":item_code,"rm_item_code":"_Test Item Home Desktop 100","item_name":"_Test Item Home Desktop 100",
"qty":2,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":200,"stock_uom":"Nos"}, "qty":20,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
{"item_code":item_code,"rm_item_code":"Test Extra Item 1","item_name":"Test Extra Item 1", {"item_code":item_code,"rm_item_code":"Test Extra Item 1","item_name":"Test Extra Item 1",
"qty":1,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":200,"stock_uom":"Nos"}] "qty":10,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
{'item_code': item_code, 'rm_item_code': 'Test Extra Item 2', 'stock_uom':'Nos',
'qty': 10, 'warehouse': '_Test Warehouse - _TC', 'item_name':'Test Extra Item 2'}]
rm_item_string = json.dumps(rm_item) rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.append('items', {
'item_code': "Test Extra Item 2",
"qty": 1,
"rate": 100,
"s_warehouse": "_Test Warehouse - _TC",
"t_warehouse": "_Test Warehouse 1 - _TC"
})
se.set_missing_values()
se.submit() se.submit()
pr = make_purchase_receipt(po.name) pr = make_purchase_receipt(po.name)
received_qty = 2
# partial receipt
pr.get('items')[0].qty = received_qty
pr.save() pr.save()
pr.submit() pr.submit()
se_items = sorted([d.item_code for d in se.get('items')]) transferred_items = sorted([d.item_code for d in se.get('items') if se.purchase_order == po.name])
supplied_items = sorted([d.rm_item_code for d in pr.get('supplied_items')]) issued_items = sorted([d.rm_item_code for d in pr.get('supplied_items')])
self.assertEquals(transferred_items, issued_items)
self.assertEquals(pr.get('items')[0].rm_supp_cost, 2000)
transferred_rm_map = frappe._dict()
for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item
for item in pr.get('supplied_items'):
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
self.assertEquals(se_items, supplied_items)
update_backflush_based_on("BOM") update_backflush_based_on("BOM")
def test_advance_payment_entry_unlink_against_purchase_order(self): def test_advance_payment_entry_unlink_against_purchase_order(self):
@ -606,6 +623,27 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(po.schedule_date, add_days(nowdate(), 2)) self.assertEqual(po.schedule_date, add_days(nowdate(), 2))
def test_po_optional_blanket_order(self):
"""
Expected result: Blanket order Ordered Quantity should only be affected on Purchase Order with against_blanket_order = 1.
Second Purchase Order should not add on to Blanket Orders Ordered Quantity.
"""
bo = make_blanket_order(blanket_order_type = "Purchasing", quantity = 10, rate = 10)
po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1)
po_doc = frappe.get_doc('Purchase Order', po.get('name'))
# To test if the PO has a Blanket Order
self.assertTrue(po_doc.items[0].blanket_order)
po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0)
po_doc = frappe.get_doc('Purchase Order', po.get('name'))
# To test if the PO does NOT have a Blanket Order
self.assertEqual(po_doc.items[0].blanket_order, None)
def make_pr_against_po(po, received_qty=0): def make_pr_against_po(po, received_qty=0):
pr = make_purchase_receipt(po) pr = make_purchase_receipt(po)
pr.get("items")[0].qty = received_qty or 5 pr.get("items")[0].qty = received_qty or 5
@ -678,7 +716,8 @@ def create_purchase_order(**args):
"qty": args.qty or 10, "qty": args.qty or 10,
"rate": args.rate or 500, "rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1), "schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get('include_exploded_items', 1) "include_exploded_items": args.get('include_exploded_items', 1),
"against_blanket_order": args.against_blanket_order
}) })
if not args.do_not_save: if not args.do_not_save:
po.insert() po.insert()

View File

@ -43,7 +43,6 @@
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"is_free_item", "is_free_item",
"is_fixed_asset",
"section_break_29", "section_break_29",
"net_rate", "net_rate",
"net_amount", "net_amount",
@ -67,6 +66,7 @@
"supplier_quotation", "supplier_quotation",
"supplier_quotation_item", "supplier_quotation_item",
"col_break5", "col_break5",
"against_blanket_order",
"blanket_order", "blanket_order",
"blanket_order_rate", "blanket_order_rate",
"item_group", "item_group",
@ -511,6 +511,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:doc.against_blanket_order",
"fieldname": "blanket_order", "fieldname": "blanket_order",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Blanket Order", "label": "Blanket Order",
@ -518,6 +519,7 @@
"options": "Blanket Order" "options": "Blanket Order"
}, },
{ {
"depends_on": "eval:doc.against_blanket_order",
"fieldname": "blanket_order_rate", "fieldname": "blanket_order_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Blanket Order Rate", "label": "Blanket Order Rate",
@ -703,16 +705,14 @@
}, },
{ {
"default": "0", "default": "0",
"fetch_from": "item_code.is_fixed_asset", "fieldname": "against_blanket_order",
"fieldname": "is_fixed_asset",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Fixed Asset", "label": "Against Blanket Order"
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-11-07 17:19:12.090355", "modified": "2019-11-19 14:10:52.865006",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@ -56,3 +56,23 @@ class Supplier(TransactionBase):
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name': if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name':
frappe.db.set(self, "supplier_name", newdn) frappe.db.set(self, "supplier_name", newdn)
def create_onboarding_docs(self, args):
defaults = frappe.defaults.get_defaults()
for i in range(1, args.get('max_count')):
supplier = args.get('supplier_name_' + str(i))
if supplier:
try:
doc = frappe.get_doc({
'doctype': self.doctype,
'supplier_name': supplier,
'supplier_group': _('Local'),
'company': defaults.get('company')
}).insert()
if args.get('supplier_email_' + str(i)):
from erpnext.selling.doctype.customer.customer import create_contact
create_contact(supplier, 'Supplier',
doc.name, args.get('supplier_email_' + str(i)))
except frappe.NameError:
pass

View File

@ -0,0 +1,49 @@
{
"add_more_button": 1,
"app": "ERPNext",
"creation": "2019-11-15 14:45:32.626641",
"docstatus": 0,
"doctype": "Setup Wizard Slide",
"domains": [],
"help_links": [
{
"label": "Supplier",
"video_id": "zsrrVDk6VBs"
}
],
"idx": 0,
"image_src": "/assets/erpnext/images/illustrations/supplier.png",
"max_count": 3,
"modified": "2019-11-26 18:26:25.498325",
"modified_by": "Administrator",
"name": "Add A Few Suppliers",
"owner": "Administrator",
"ref_doctype": "Supplier",
"slide_desc": "",
"slide_fields": [
{
"align": "",
"fieldname": "supplier_name",
"fieldtype": "Data",
"label": "Supplier Name",
"placeholder": "",
"reqd": 1
},
{
"align": "",
"fieldtype": "Column Break",
"reqd": 0
},
{
"align": "",
"fieldname": "supplier_email",
"fieldtype": "Data",
"label": "Supplier Email",
"reqd": 1
}
],
"slide_order": 50,
"slide_title": "Add A Few Suppliers",
"slide_type": "Create",
"submit_method": ""
}

View File

@ -197,6 +197,11 @@ def get_data():
"name": "Bank Reconciliation Statement", "name": "Bank Reconciliation Statement",
"is_query_report": True, "is_query_report": True,
"doctype": "Journal Entry" "doctype": "Journal Entry"
},{
"type": "page",
"name": "bank-reconciliation",
"label": _("Bank Reconciliation"),
"icon": "fa fa-bar-chart"
}, },
{ {
"type": "report", "type": "report",

View File

@ -241,6 +241,10 @@ def get_data():
"type": "doctype", "type": "doctype",
"name": "Quality Inspection Template", "name": "Quality Inspection Template",
}, },
{
"type": "doctype",
"name": "Quick Stock Balance",
},
] ]
}, },
{ {

View File

@ -61,7 +61,6 @@ class AccountsController(TransactionBase):
_('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1) _('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1)
def validate(self): def validate(self):
if not self.get('is_return'): if not self.get('is_return'):
self.validate_qty_is_not_zero() self.validate_qty_is_not_zero()
@ -100,11 +99,23 @@ class AccountsController(TransactionBase):
if self.is_return: if self.is_return:
self.validate_qty() self.validate_qty()
else:
self.validate_deferred_start_and_end_date()
validate_regional(self) validate_regional(self)
if self.doctype != 'Material Request': if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self) apply_pricing_rule_on_transaction(self)
def validate_deferred_start_and_end_date(self):
for d in self.items:
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
if not (d.service_start_date and d.service_end_date):
frappe.throw(_("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx))
elif getdate(d.service_start_date) > getdate(d.service_end_date):
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_end_date):
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
def validate_invoice_documents_schedule(self): def validate_invoice_documents_schedule(self):
self.validate_payment_schedule_dates() self.validate_payment_schedule_dates()
self.set_due_date() self.set_due_date()
@ -415,9 +426,10 @@ class AccountsController(TransactionBase):
return gl_dict return gl_dict
def validate_qty_is_not_zero(self): def validate_qty_is_not_zero(self):
for item in self.items: if self.doctype != "Purchase Receipt":
if not item.qty: for item in self.items:
frappe.throw(_("Item quantity can not be zero")) if not item.qty:
frappe.throw(_("Item quantity can not be zero"))
def validate_account_currency(self, account, account_currency=None): def validate_account_currency(self, account, account_currency=None):
valid_currency = [self.company_currency] valid_currency = [self.company_currency]

View File

@ -221,7 +221,7 @@ class BuyingController(StockController):
"backflush_raw_materials_of_subcontract_based_on") "backflush_raw_materials_of_subcontract_based_on")
if (self.doctype == 'Purchase Receipt' and if (self.doctype == 'Purchase Receipt' and
backflush_raw_materials_based_on != 'BOM'): backflush_raw_materials_based_on != 'BOM'):
self.update_raw_materials_supplied_based_on_stock_entries(raw_material_table) self.update_raw_materials_supplied_based_on_stock_entries()
else: else:
for item in self.get("items"): for item in self.get("items"):
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@ -241,41 +241,95 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"): if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', []) self.set('supplied_items', [])
def update_raw_materials_supplied_based_on_stock_entries(self, raw_material_table): def update_raw_materials_supplied_based_on_stock_entries(self):
self.set(raw_material_table, []) self.set('supplied_items', [])
purchase_orders = [d.purchase_order for d in self.items]
if purchase_orders:
items = get_subcontracted_raw_materials_from_se(purchase_orders)
backflushed_raw_materials = get_backflushed_subcontracted_raw_materials_from_se(purchase_orders, self.name)
for d in items: purchase_orders = set([d.purchase_order for d in self.items])
qty = d.qty - backflushed_raw_materials.get(d.item_code, 0)
rm = self.append(raw_material_table, {})
rm.rm_item_code = d.item_code
rm.item_name = d.item_name
rm.main_item_code = d.main_item_code
rm.description = d.description
rm.stock_uom = d.stock_uom
rm.required_qty = qty
rm.consumed_qty = qty
rm.serial_no = d.serial_no
rm.batch_no = d.batch_no
# get raw materials rate # qty of raw materials backflushed (for each item per purchase order)
from erpnext.stock.utils import get_incoming_rate backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
rm.rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * qty,
"serial_no": rm.serial_no
})
if not rm.rate:
rm.rate = get_valuation_rate(d.item_code, self.supplier_warehouse,
self.doctype, self.name, currency=self.company_currency, company = self.company)
rm.amount = qty * flt(rm.rate) # qty of "finished good" item yet to be received
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
for item in self.get('items'):
# reset raw_material cost
item.rm_supp_cost = 0
# qty of raw materials transferred to the supplier
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
item_key = '{}{}'.format(item.item_code, item.purchase_order)
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
raw_material_data = backflushed_raw_materials_map.get(item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_nos', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
transferred_qty = raw_material.qty
rm_qty_to_be_consumed = transferred_qty - consumed_qty
# backflush all remaining transferred qty in the last Purchase Receipt
if fg_yet_to_be_received == item.qty:
qty = rm_qty_to_be_consumed
else:
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
qty = frappe.utils.ceil(qty)
if qty > rm_qty_to_be_consumed:
qty = rm_qty_to_be_consumed
if not qty: continue
if raw_material.serial_nos:
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
rm.required_qty = qty
rm.consumed_qty = qty
if not raw_material_data.get('non_stock_item'):
from erpnext.stock.utils import get_incoming_rate
rm.rate = get_incoming_rate({
"item_code": raw_material_data.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * qty,
"serial_no": rm.serial_no
})
if not rm.rate:
rm.rate = get_valuation_rate(raw_material_data.item_code, self.supplier_warehouse,
self.doctype, self.name, currency=self.company_currency, company=self.company)
rm.amount = qty * flt(rm.rate)
fg_item_doc.rm_supp_cost += rm.amount
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table): def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1 exploded_item = 1
@ -387,9 +441,11 @@ class BuyingController(StockController):
item_codes = list(set(item.item_code for item in item_codes = list(set(item.item_code for item in
self.get("items"))) self.get("items")))
if item_codes: if item_codes:
self._sub_contracted_items = [r[0] for r in frappe.db.sql("""select name items = frappe.get_all('Item', filters={
from `tabItem` where name in (%s) and is_sub_contracted_item=1""" % \ 'name': ['in', item_codes],
(", ".join((["%s"]*len(item_codes))),), item_codes)] 'is_sub_contracted_item': 1
})
self._sub_contracted_items = [item.name for item in items]
return self._sub_contracted_items return self._sub_contracted_items
@ -722,28 +778,72 @@ def get_items_from_bom(item_code, bom, exploded_item=1):
return bom_items return bom_items
def get_subcontracted_raw_materials_from_se(purchase_orders): def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
return frappe.db.sql(""" common_query = """
select SELECT
sed.item_name, sed.item_code, sum(sed.qty) as qty, sed.description, sed.item_code AS rm_item_code,
sed.stock_uom, sed.subcontracted_item as main_item_code, sed.serial_no, sed.batch_no SUM(sed.qty) AS qty,
from `tabStock Entry` se,`tabStock Entry Detail` sed sed.description,
where sed.stock_uom,
se.name = sed.parent and se.docstatus=1 and se.purpose='Send to Subcontractor' sed.subcontracted_item AS main_item_code,
and se.purchase_order in (%s) and ifnull(sed.t_warehouse, '') != '' {serial_no_concat_syntax} AS serial_nos,
group by sed.item_code, sed.t_warehouse {batch_no_concat_syntax} AS batch_nos
""" % (','.join(['%s'] * len(purchase_orders))), tuple(purchase_orders), as_dict=1) FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
AND sed.subcontracted_item = %s
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
)
}, (purchase_order, fg_item), as_dict=1)
def get_backflushed_subcontracted_raw_materials_from_se(purchase_orders, purchase_receipt): return raw_materials
return frappe._dict(frappe.db.sql("""
select def get_backflushed_subcontracted_raw_materials(purchase_orders):
prsi.rm_item_code as item_code, sum(prsi.consumed_qty) as qty common_query = """
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi SELECT
where CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key,
pr.name = pri.parent and pr.name = prsi.parent and pri.purchase_order in (%s) SUM(prsi.consumed_qty) AS qty,
and pri.item_code = prsi.main_item_code and pr.name != '%s' and pr.docstatus = 1 {serial_no_concat_syntax} AS serial_nos,
group by prsi.rm_item_code {batch_no_concat_syntax} AS batch_nos
""" % (','.join(['%s'] * len(purchase_orders)), purchase_receipt), tuple(purchase_orders))) FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi
WHERE
pr.name = pri.parent
AND pr.name = prsi.parent
AND pri.purchase_order IN %s
AND pri.item_code = prsi.main_item_code
AND pr.docstatus = 1
GROUP BY prsi.rm_item_code, pri.purchase_order
"""
backflushed_raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')"
)
}, (purchase_orders, ), as_dict=1)
backflushed_raw_materials_map = frappe._dict()
for item in backflushed_raw_materials:
backflushed_raw_materials_map.setdefault(item.item_key, item)
return backflushed_raw_materials_map
def get_asset_item_details(asset_items): def get_asset_item_details(asset_items):
asset_items_data = {} asset_items_data = {}
@ -776,3 +876,125 @@ def validate_item_type(doc, fieldname, message):
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master".format(items, message)) error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master".format(items, message))
frappe.throw(error_message) frappe.throw(error_message)
def get_qty_to_be_received(purchase_orders):
return frappe._dict(frappe.db.sql("""
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
FROM `tabPurchase Order Item` poi
WHERE
poi.`parent` in %s
GROUP BY poi.`item_code`, poi.`parent`
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
""", (purchase_orders)))
def get_non_stock_items(purchase_order, fg_item_code):
return frappe.db.sql("""
SELECT
pois.main_item_code,
pois.rm_item_code,
item.description,
pois.required_qty AS qty,
pois.rate,
1 as non_stock_item,
pois.stock_uom
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
WHERE
pois.`rm_item_code` = item.`name`
AND item.is_stock_item = 0
AND pois.`parent` = %s
AND pois.`main_item_code` = %s
""", (purchase_order, fg_item_code), as_dict=1)
def set_serial_nos(raw_material, consumed_serial_nos, qty):
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
set(get_serial_nos(consumed_serial_nos))
if serial_nos and qty <= len(serial_nos):
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
def get_transferred_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
transferred_batch_qty_map = {}
transferred_batches = frappe.db.sql("""
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
sed.item_code
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND sed.subcontracted_item = %s
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
sed.item_code
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
def get_backflushed_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
backflushed_batch_qty_map = {}
backflushed_batches = frappe.db.sql("""
SELECT
pris.batch_no,
SUM(pris.consumed_qty) AS qty,
pris.rm_item_code AS item_code
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
WHERE
pr.name = pri.parent
AND pri.parent = pris.parent
AND pri.purchase_order = %s
AND pri.item_code = pris.main_item_code
AND pr.docstatus = 1
AND pris.main_item_code = %s
AND pris.batch_no IS NOT NULL
GROUP BY
pris.rm_item_code, pris.batch_no
""", (purchase_order, fg_item), as_dict=1)
for batch_data in backflushed_batches:
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
return backflushed_batch_qty_map
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {})
available_batches = []
for (batch, transferred_qty) in transferred_batches.items():
backflushed_qty = backflushed_batches.get(batch, 0)
available_qty = transferred_qty - backflushed_qty
if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty})
break
else:
available_batches.append({'batch': batch, 'qty': available_qty})
required_qty -= available_qty
return available_batches

View File

@ -41,7 +41,8 @@ class EmailCampaign(Document):
email_campaign_exists = frappe.db.exists("Email Campaign", { email_campaign_exists = frappe.db.exists("Email Campaign", {
"campaign_name": self.campaign_name, "campaign_name": self.campaign_name,
"recipient": self.recipient, "recipient": self.recipient,
"status": ("in", ["In Progress", "Scheduled"]) "status": ("in", ["In Progress", "Scheduled"]),
"name": ("!=", self.name)
}) })
if email_campaign_exists: if email_campaign_exists:
frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient))
@ -78,7 +79,7 @@ def send_mail(entry, email_campaign):
comm = make( comm = make(
doctype = "Email Campaign", doctype = "Email Campaign",
name = email_campaign.name, name = email_campaign.name,
subject = email_template.get("subject"), subject = frappe.render_template(email_template.get("subject"), context),
content = frappe.render_template(email_template.get("response"), context), content = frappe.render_template(email_template.get("response"), context),
sender = sender, sender = sender,
recipients = recipient, recipients = recipient,

View File

@ -130,10 +130,10 @@ class Opportunity(TransactionBase):
def has_lost_quotation(self): def has_lost_quotation(self):
lost_quotation = frappe.db.sql(""" lost_quotation = frappe.db.sql("""
select q.name select name
from `tabQuotation` q, `tabQuotation Item` qi from `tabQuotation`
where q.name = qi.parent and q.docstatus=1 where docstatus=1
and qi.prevdoc_docname =%s and q.status = 'Lost' and opportunity =%s and status = 'Lost'
""", self.name) """, self.name)
if lost_quotation: if lost_quotation:
if self.has_active_quotation(): if self.has_active_quotation():

View File

@ -71,7 +71,7 @@ class ProgramEnrollment(Document):
def create_course_enrollments(self): def create_course_enrollments(self):
student = frappe.get_doc("Student", self.student) student = frappe.get_doc("Student", self.student)
program = frappe.get_doc("Program", self.program) program = frappe.get_doc("Program", self.program)
course_list = [course.course for course in program.get_all_children()] course_list = [course.course for course in program.courses]
for course_name in course_list: for course_name in course_list:
student.enroll_in_course(course_name=course_name, program_enrollment=self.name) student.enroll_in_course(course_name=course_name, program_enrollment=self.name)

View File

@ -40,7 +40,7 @@ class Student(Document):
frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant)) frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant))
def after_insert(self): def after_insert(self):
if not frappe.get_single('Education Settings').user_creation_skip: if not frappe.get_single('Education Settings').get('user_creation_skip'):
self.create_student_user() self.create_student_user()
def create_student_user(self): def create_student_user(self):

View File

@ -63,10 +63,11 @@ def updating_rate(self):
item_code=%s""",(self.template, self.rate, self.item)) item_code=%s""",(self.template, self.rate, self.item))
def create_item_from_template(doc): def create_item_from_template(doc):
disabled = 1
if(doc.is_billable == 1): if(doc.is_billable == 1):
disabled = 0 disabled = 0
else:
disabled = 1
#insert item #insert item
item = frappe.get_doc({ item = frappe.get_doc({
"doctype": "Item", "doctype": "Item",

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import today, now_datetime from frappe.utils import today, now_datetime, getdate
from frappe.model.document import Document from frappe.model.document import Document
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
@ -15,11 +15,20 @@ class InpatientRecord(Document):
frappe.db.set_value("Patient", self.patient, "inpatient_record", self.name) frappe.db.set_value("Patient", self.patient, "inpatient_record", self.name)
def validate(self): def validate(self):
self.validate_dates()
self.validate_already_scheduled_or_admitted() self.validate_already_scheduled_or_admitted()
if self.status == "Discharged": if self.status == "Discharged":
frappe.db.set_value("Patient", self.patient, "inpatient_status", None) frappe.db.set_value("Patient", self.patient, "inpatient_status", None)
frappe.db.set_value("Patient", self.patient, "inpatient_record", None) frappe.db.set_value("Patient", self.patient, "inpatient_record", None)
def validate_dates(self):
if (getdate(self.scheduled_date) < getdate(today())) or \
(getdate(self.admitted_datetime) < getdate(today())):
frappe.throw(_("Scheduled and Admitted dates can not be less than today"))
if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or \
(getdate(self.discharge_date) < getdate(self.scheduled_date)):
frappe.throw(_("Expected and Discharge dates cannot be less than Admission Schedule date"))
def validate_already_scheduled_or_admitted(self): def validate_already_scheduled_or_admitted(self):
query = """ query = """
select name, status select name, status

View File

@ -40,8 +40,6 @@ after_install = "erpnext.setup.install.after_install"
boot_session = "erpnext.startup.boot.boot_session" boot_session = "erpnext.startup.boot.boot_session"
notification_config = "erpnext.startup.notifications.get_notification_config" notification_config = "erpnext.startup.notifications.get_notification_config"
get_help_messages = "erpnext.utilities.activation.get_help_messages" get_help_messages = "erpnext.utilities.activation.get_help_messages"
get_user_progress_slides = "erpnext.utilities.user_progress.get_user_progress_slides"
update_and_get_user_progress = "erpnext.utilities.user_progress_utils.update_default_domain_actions_and_get_state"
leaderboards = "erpnext.startup.leaderboard.get_leaderboards" leaderboards = "erpnext.startup.leaderboard.get_leaderboards"
@ -302,7 +300,7 @@ scheduler_events = {
"erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status",
"erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts", "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts",
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status", "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status",
"erpnext.selling.doctype.quotation.set_expired_status" "erpnext.selling.doctype.quotation.quotation.set_expired_status"
], ],
"daily_long": [ "daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.setup.doctype.email_digest.email_digest.send",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr
from frappe.model.naming import set_name_by_naming_series from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.permissions import add_user_permission, remove_user_permission, \
@ -152,8 +152,8 @@ class Employee(NestedSet):
elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)): elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)):
throw(_("Date Of Retirement must be greater than Date of Joining")) throw(_("Date Of Retirement must be greater than Date of Joining"))
elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) <= getdate(self.date_of_joining)): elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) < getdate(self.date_of_joining)):
throw(_("Relieving Date must be greater than Date of Joining")) throw(_("Relieving Date must be greater than or equal to Date of Joining"))
elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)): elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)):
throw(_("Contract End Date must be greater than Date of Joining")) throw(_("Contract End Date must be greater than Date of Joining"))
@ -218,8 +218,8 @@ class Employee(NestedSet):
def reset_employee_emails_cache(self): def reset_employee_emails_cache(self):
prev_doc = self.get_doc_before_save() or {} prev_doc = self.get_doc_before_save() or {}
cell_number = self.get('cell_number') cell_number = cstr(self.get('cell_number'))
prev_number = prev_doc.get('cell_number') prev_number = cstr(prev_doc.get('cell_number'))
if (cell_number != prev_number or if (cell_number != prev_number or
self.get('user_id') != prev_doc.get('user_id')): self.get('user_id') != prev_doc.get('user_id')):
frappe.cache().hdel('employees_with_number', cell_number) frappe.cache().hdel('employees_with_number', cell_number)

View File

@ -31,7 +31,11 @@ frappe.ui.form.on('Payroll Entry', {
} }
if ((frm.doc.employees || []).length) { if ((frm.doc.employees || []).length) {
frm.page.set_primary_action(__('Create Salary Slips'), () => { frm.page.set_primary_action(__('Create Salary Slips'), () => {
frm.save('Submit'); frm.save('Submit').then(()=>{
frm.page.clear_primary_action();
frm.refresh();
frm.events.refresh(frm);
});
}); });
} }
} }

View File

@ -1,231 +1,82 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-12-20 15:32:25.078334", "creation": "2016-12-20 15:32:25.078334",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"payment_date",
"principal_amount",
"interest_amount",
"total_payment",
"balance_loan_amount",
"paid"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"fieldname": "payment_date", "fieldname": "payment_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Payment Date"
"label": "Payment Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"fieldname": "principal_amount", "fieldname": "principal_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Principal Amount", "label": "Principal Amount",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"fieldname": "interest_amount", "fieldname": "interest_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Interest Amount", "label": "Interest Amount",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"fieldname": "total_payment", "fieldname": "total_payment",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Payment", "label": "Total Payment",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"fieldname": "balance_loan_amount", "fieldname": "balance_loan_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Balance Loan Amount", "label": "Balance Loan Amount",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "paid", "fieldname": "paid",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Paid", "label": "Paid",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "modified": "2019-10-29 11:45:10.694557",
"modified": "2018-03-30 17:37:31.834792",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Repayment Schedule", "name": "Repayment Schedule",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0
} }

View File

@ -44,6 +44,8 @@ def make_sales_order(source_name):
target.item_name = item.get("item_name") target.item_name = item.get("item_name")
target.description = item.get("description") target.description = item.get("description")
target.uom = item.get("stock_uom") target.uom = item.get("stock_uom")
target.against_blanket_order = 1
target.blanket_order = source_name
target_doc = get_mapped_doc("Blanket Order", source_name, { target_doc = get_mapped_doc("Blanket Order", source_name, {
"Blanket Order": { "Blanket Order": {
@ -71,6 +73,8 @@ def make_purchase_order(source_name):
target.description = item.get("description") target.description = item.get("description")
target.uom = item.get("stock_uom") target.uom = item.get("stock_uom")
target.warehouse = item.get("default_warehouse") target.warehouse = item.get("default_warehouse")
target.against_blanket_order = 1
target.blanket_order = source_name
target_doc = get_mapped_doc("Blanket Order", source_name, { target_doc = get_mapped_doc("Blanket Order", source_name, {
"Blanket Order": { "Blanket Order": {

View File

@ -606,6 +606,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
item.image, item.image,
bom.project, bom.project,
item.stock_uom, item.stock_uom,
item.item_group,
item.allow_alternative_item, item.allow_alternative_item,
item_default.default_warehouse, item_default.default_warehouse,
item_default.expense_account as expense_account, item_default.expense_account as expense_account,

View File

@ -9,6 +9,7 @@ from frappe import _
from six import string_types from six import string_types
from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
from frappe.model.document import Document from frappe.model.document import Document
import click
class BOMUpdateTool(Document): class BOMUpdateTool(Document):
def replace_bom(self): def replace_bom(self):
@ -17,7 +18,8 @@ class BOMUpdateTool(Document):
frappe.cache().delete_key('bom_children') frappe.cache().delete_key('bom_children')
bom_list = self.get_parent_boms(self.new_bom) bom_list = self.get_parent_boms(self.new_bom)
updated_bom = [] updated_bom = []
with click.progressbar(bom_list) as bom_list:
pass
for bom in bom_list: for bom in bom_list:
try: try:
bom_obj = frappe.get_cached_doc('BOM', bom) bom_obj = frappe.get_cached_doc('BOM', bom)

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import datetime
from frappe import _ from frappe import _
from frappe.utils import flt, time_diff_in_hours, get_datetime
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
get_time, add_to_date, time_diff, add_days, get_datetime_str)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
class OverlapError(frappe.ValidationError): pass
class JobCard(Document): class JobCard(Document):
def validate(self): def validate(self):
@ -26,7 +32,7 @@ class JobCard(Document):
data = self.get_overlap_for(d) data = self.get_overlap_for(d)
if data: if data:
frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}") frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}")
.format(d.idx, self.name, data.name)) .format(d.idx, self.name, data.name), OverlapError)
if d.from_time and d.to_time: if d.from_time and d.to_time:
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
@ -35,27 +41,120 @@ class JobCard(Document):
if d.completed_qty: if d.completed_qty:
self.total_completed_qty += d.completed_qty self.total_completed_qty += d.completed_qty
def get_overlap_for(self, args): def get_overlap_for(self, args, check_next_available_slot=False):
existing = frappe.db.sql("""select jc.name as name from production_capacity = 1
if self.workstation:
production_capacity = frappe.get_cached_value("Workstation",
self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s "
if self.employee:
# override capacity for employee
production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s "
extra_cond = ''
if check_next_available_slot:
extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)"
existing = frappe.db.sql("""select jc.name as name, jctl.to_time from
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and `tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
( (
(%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or (%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or
(%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or (%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time)) (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
and jctl.name!=%(name)s )
and jc.name!=%(parent)s and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
and jc.docstatus < 2 order by jctl.to_time desc limit 1""".format(extra_cond, validate_overlap_for),
and jc.employee = %(employee)s """,
{ {
"from_time": args.from_time, "from_time": args.from_time,
"to_time": args.to_time, "to_time": args.to_time,
"name": args.name or "No Name", "name": args.name or "No Name",
"parent": args.parent or "No Name", "parent": args.parent or "No Name",
"employee": self.employee "employee": self.employee,
"workstation": self.workstation
}, as_dict=True) }, as_dict=True)
if existing and production_capacity > len(existing):
return
return existing[0] if existing else None return existing[0] if existing else None
def schedule_time_logs(self, row):
row.remaining_time_in_mins = row.time_in_mins
while row.remaining_time_in_mins > 0:
args = frappe._dict({
"from_time": row.planned_start_time,
"to_time": row.planned_end_time
})
self.validate_overlap_for_workstation(args, row)
self.check_workstation_time(row)
def validate_overlap_for_workstation(self, args, row):
# get the last record based on the to time from the job card
data = self.get_overlap_for(args, check_next_available_slot=True)
if data:
row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
def check_workstation_time(self, row):
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
if (not workstation_doc.working_hours or
cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime"))):
row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time,
row.planned_start_time)
self.update_time_logs(row)
return
start_date = getdate(row.planned_start_time)
start_time = get_time(row.planned_start_time)
new_start_date = workstation_doc.validate_workstation_holiday(start_date)
if new_start_date != start_date:
row.planned_start_time = datetime.datetime.combine(new_start_date, start_time)
start_date = new_start_date
total_idx = len(workstation_doc.working_hours)
for i, time_slot in enumerate(workstation_doc.working_hours):
workstation_start_time = datetime.datetime.combine(start_date, get_time(time_slot.start_time))
workstation_end_time = datetime.datetime.combine(start_date, get_time(time_slot.end_time))
if (get_datetime(row.planned_start_time) >= workstation_start_time and
get_datetime(row.planned_start_time) <= workstation_end_time):
time_in_mins = time_diff_in_minutes(workstation_end_time, row.planned_start_time)
# If remaining time fit in workstation time logs else split hours as per workstation time
if time_in_mins > row.remaining_time_in_mins:
row.planned_end_time = add_to_date(row.planned_start_time,
minutes=row.remaining_time_in_mins)
row.remaining_time_in_mins = 0
else:
row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins)
row.remaining_time_in_mins -= time_in_mins
self.update_time_logs(row)
if total_idx != (i+1) and row.remaining_time_in_mins > 0:
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[i+1].start_time))
if row.remaining_time_in_mins > 0:
start_date = add_days(start_date, 1)
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time))
def update_time_logs(self, row):
self.append("time_logs", {
"from_time": row.planned_start_time,
"to_time": row.planned_end_time,
"completed_qty": 0,
"time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time),
})
def get_required_items(self): def get_required_items(self):
if not self.get('work_order'): if not self.get('work_order'):
return return
@ -251,3 +350,6 @@ def make_stock_entry(source_name, target_doc=None):
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doclist return doclist
def time_diff_in_minutes(string_ed_date, string_st_date):
return time_diff(string_ed_date, string_st_date).total_seconds() / 60

View File

@ -1,585 +1,178 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2014-11-27 14:12:07.542534", "creation": "2014-11-27 14:12:07.542534",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"raw_materials_consumption_section",
"material_consumption",
"column_break_3",
"backflush_raw_materials_based_on",
"capacity_planning",
"disable_capacity_planning",
"allow_overtime",
"allow_production_on_holidays",
"column_break_5",
"capacity_planning_for_days",
"mins_between_operations",
"section_break_6",
"default_wip_warehouse",
"default_fg_warehouse",
"column_break_11",
"default_scrap_warehouse",
"over_production_for_sales_and_work_order_section",
"overproduction_percentage_for_sales_order",
"column_break_16",
"overproduction_percentage_for_work_order",
"other_settings_section",
"update_bom_costs_automatically"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "capacity_planning", "fieldname": "capacity_planning",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Capacity Planning"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Capacity Planning",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "depends_on": "eval:!doc.disable_capacity_planning",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Disables creation of time logs against Work Orders. Operations shall not be tracked against Work Order",
"fieldname": "disable_capacity_planning",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disable Capacity Planning and Time Tracking",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Plan time logs outside Workstation Working Hours.", "description": "Plan time logs outside Workstation Working Hours.",
"fieldname": "allow_overtime", "fieldname": "allow_overtime",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Allow Overtime"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Overtime",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "depends_on": "eval:!doc.disable_capacity_planning",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "allow_production_on_holidays", "fieldname": "allow_production_on_holidays",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Allow Production on Holidays"
"label": "Allow Production on Holidays",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "30", "default": "30",
"depends_on": "eval:!doc.disable_capacity_planning",
"description": "Try planning operations for X days in advance.", "description": "Try planning operations for X days in advance.",
"fieldname": "capacity_planning_for_days", "fieldname": "capacity_planning_for_days",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "label": "Capacity Planning For (Days)"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Capacity Planning For (Days)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "depends_on": "eval:!doc.disable_capacity_planning",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Default 10 mins", "description": "Default 10 mins",
"fieldname": "mins_between_operations", "fieldname": "mins_between_operations",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "label": "Time Between Operations (in mins)"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Time Between Operations (in mins)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Default Warehouses for Production"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "overproduction_percentage_for_sales_order", "fieldname": "overproduction_percentage_for_sales_order",
"fieldtype": "Percent", "fieldtype": "Percent",
"hidden": 0, "label": "Overproduction Percentage For Sales Order"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Overproduction Percentage For Sales Order",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "overproduction_percentage_for_work_order", "fieldname": "overproduction_percentage_for_work_order",
"fieldtype": "Percent", "fieldtype": "Percent",
"hidden": 0, "label": "Overproduction Percentage For Work Order"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Overproduction Percentage For Work Order",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "BOM", "default": "BOM",
"fieldname": "backflush_raw_materials_based_on", "fieldname": "backflush_raw_materials_based_on",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Backflush Raw Materials Based On", "label": "Backflush Raw Materials Based On",
"length": 0, "options": "BOM\nMaterial Transferred for Manufacture"
"no_copy": 0,
"options": "BOM\nMaterial Transferred for Manufacture",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Allow multiple Material Consumption against a Work Order", "description": "Allow multiple Material Consumption against a Work Order",
"fieldname": "material_consumption", "fieldname": "material_consumption",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Allow Multiple Material Consumption"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Multiple Material Consumption",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.", "description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.",
"fieldname": "update_bom_costs_automatically", "fieldname": "update_bom_costs_automatically",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Update BOM Cost Automatically"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Update BOM Cost Automatically",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_wip_warehouse", "fieldname": "default_wip_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Work In Progress Warehouse", "label": "Default Work In Progress Warehouse",
"length": 0, "options": "Warehouse"
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_fg_warehouse", "fieldname": "default_fg_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Finished Goods Warehouse", "label": "Default Finished Goods Warehouse",
"length": 0, "options": "Warehouse"
"no_copy": 0, },
"options": "Warehouse", {
"permlevel": 0, "default": "0",
"precision": "", "fieldname": "disable_capacity_planning",
"print_hide": 0, "fieldtype": "Check",
"print_hide_if_no_value": 0, "label": "Disable Capacity Planning"
"read_only": 0, },
"remember_last_selected_value": 0, {
"report_hide": 0, "fieldname": "default_scrap_warehouse",
"reqd": 0, "fieldtype": "Link",
"search_index": 0, "label": "Default Scrap Warehouse",
"set_only_once": 0, "options": "Warehouse"
"translatable": 0, },
"unique": 0 {
"fieldname": "over_production_for_sales_and_work_order_section",
"fieldtype": "Section Break",
"label": "Over Production for Sales and Work Order"
},
{
"fieldname": "raw_materials_consumption_section",
"fieldtype": "Section Break",
"label": "Raw Materials Consumption"
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "other_settings_section",
"fieldtype": "Section Break",
"label": "Other Settings"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-wrench", "icon": "icon-wrench",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "modified": "2019-11-26 13:10:45.569341",
"max_attachments": 0,
"menu_index": 0,
"modified": "2018-05-28 00:46:25.310621",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing Settings", "name": "Manufacturing Settings",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0,
"role": "Manufacturing Manager", "role": "Manufacturing Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0
} }

View File

@ -5,10 +5,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_days, cint from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.work_order.work_order \ from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
import make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@ -307,14 +307,50 @@ class TestWorkOrder(unittest.TestCase):
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data: if data:
frappe.db.set_value("Manufacturing Settings",
None, "disable_capacity_planning", 0)
bom, bom_item = data bom, bom_item = data
bom_doc = frappe.get_doc('BOM', bom) bom_doc = frappe.get_doc('BOM', bom)
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
self.assertTrue(work_order.planned_end_date)
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
self.assertEqual(len(job_cards), len(bom_doc.operations)) self.assertEqual(len(job_cards), len(bom_doc.operations))
def test_capcity_planning(self):
frappe.db.set_value("Manufacturing Settings", None, {
"disable_capacity_planning": 0,
"capacity_planning_for_days": 1
})
data = frappe.get_cached_value('BOM', {'docstatus': 1, 'item': '_Test FG Item 2',
'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data:
bom, bom_item = data
planned_start_date = add_months(today(), months=-1)
work_order = make_wo_order_test_record(item=bom_item,
qty=10, bom_no=bom, planned_start_date=planned_start_date)
work_order1 = make_wo_order_test_record(item=bom_item,
qty=30, bom_no=bom, planned_start_date=planned_start_date, do_not_submit=1)
self.assertRaises(CapacityError, work_order1.submit)
frappe.db.set_value("Manufacturing Settings", None, {
"capacity_planning_for_days": 30
})
work_order1.reload()
work_order1.submit()
self.assertTrue(work_order1.docstatus, 1)
work_order1.cancel()
work_order.cancel()
def test_work_order_with_non_transfer_item(self): def test_work_order_with_non_transfer_item(self):
items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0} items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0}
for item, allow_transfer in items.items(): for item, allow_transfer in items.items():
@ -371,14 +407,12 @@ def make_wo_order_test_record(**args):
wo_order.skip_transfer=1 wo_order.skip_transfer=1
wo_order.get_items_and_operations_from_bom() wo_order.get_items_and_operations_from_bom()
wo_order.sales_order = args.sales_order or None wo_order.sales_order = args.sales_order or None
wo_order.planned_start_date = args.planned_start_date or now()
if args.source_warehouse: if args.source_warehouse:
for item in wo_order.get("required_items"): for item in wo_order.get("required_items"):
item.source_warehouse = args.source_warehouse item.source_warehouse = args.source_warehouse
if args.planned_start_date:
wo_order.planned_start_date = args.planned_start_date
if not args.do_not_save: if not args.do_not_save:
wo_order.insert() wo_order.insert()

View File

@ -551,6 +551,7 @@ erpnext.work_order = {
if (!r.exe) { if (!r.exe) {
frm.set_value("wip_warehouse", r.message.wip_warehouse); frm.set_value("wip_warehouse", r.message.wip_warehouse);
frm.set_value("fg_warehouse", r.message.fg_warehouse); frm.set_value("fg_warehouse", r.message.fg_warehouse);
frm.set_value("scrap_warehouse", r.message.scrap_warehouse);
} }
} }
}); });

View File

@ -12,7 +12,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from erpnext.stock.doctype.item.item import validate_end_of_life from erpnext.stock.doctype.item.item import validate_end_of_life
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError
from erpnext.projects.doctype.timesheet.timesheet import OverlapError from erpnext.manufacturing.doctype.job_card.job_card import OverlapError
from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
@ -22,6 +22,7 @@ from erpnext.utilities.transaction_base import validate_uom_is_integer
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
class OverProductionError(frappe.ValidationError): pass class OverProductionError(frappe.ValidationError): pass
class CapacityError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass
class ItemHasVariantError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass
@ -260,12 +261,50 @@ class WorkOrder(Document):
self.update_reserved_qty_for_production() self.update_reserved_qty_for_production()
def create_job_card(self): def create_job_card(self):
for row in self.operations: manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
for i, row in enumerate(self.operations):
self.set_operation_start_end_time(i, row)
if not row.workstation: if not row.workstation:
frappe.throw(_("Row {0}: select the workstation against the operation {1}") frappe.throw(_("Row {0}: select the workstation against the operation {1}")
.format(row.idx, row.operation)) .format(row.idx, row.operation))
create_job_card(self, row, auto_create=True) original_start_time = row.planned_start_time
job_card_doc = create_job_card(self, row,
enable_capacity_planning=enable_capacity_planning, auto_create=True)
if enable_capacity_planning and job_card_doc:
row.planned_start_time = job_card_doc.time_logs[0].from_time
row.planned_end_time = job_card_doc.time_logs[-1].to_time
if date_diff(row.planned_start_time, original_start_time) > plan_days:
frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
.format(plan_days, row.operation), CapacityError)
row.db_update()
planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date:
self.db_set("planned_end_date", planned_end_date)
def set_operation_start_end_time(self, idx, row):
"""Set start and end time for given operation. If first operation, set start as
`planned_start_date`, else add time diff to end time of earlier operation."""
if idx==0:
# first operation at planned_start date
row.planned_start_time = self.planned_start_date
else:
row.planned_start_time = get_datetime(self.operations[idx-1].planned_end_time)\
+ get_mins_between_operations()
row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta(minutes = row.time_in_mins)
if row.planned_start_time == row.planned_end_time:
frappe.throw(_("Capacity Planning Error, planned start time can not be same as end time"))
def validate_cancel(self): def validate_cancel(self):
if self.status == "Stopped": if self.status == "Stopped":
@ -327,9 +366,8 @@ class WorkOrder(Document):
"""Fetch operations from BOM and set in 'Work Order'""" """Fetch operations from BOM and set in 'Work Order'"""
self.set('operations', []) self.set('operations', [])
if not self.bom_no \ if not self.bom_no:
or cint(frappe.db.get_single_value("Manufacturing Settings", "disable_capacity_planning")): return
return
if self.use_multi_level_bom: if self.use_multi_level_bom:
bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree()
@ -681,11 +719,13 @@ def make_stock_entry(work_order_id, purpose, qty=None):
@frappe.whitelist() @frappe.whitelist()
def get_default_warehouse(): def get_default_warehouse():
wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", doc = frappe.get_cached_doc("Manufacturing Settings")
"default_wip_warehouse")
fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", return {
"default_fg_warehouse") "wip_warehouse": doc.default_wip_warehouse,
return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse} "fg_warehouse": doc.default_fg_warehouse,
"scrap_warehouse": doc.default_scrap_warehouse
}
@frappe.whitelist() @frappe.whitelist()
def stop_unstop(work_order, status): def stop_unstop(work_order, status):
@ -721,7 +761,7 @@ def make_job_card(work_order, operation, workstation, qty=0):
if row: if row:
return create_job_card(work_order, row, qty) return create_job_card(work_order, row, qty)
def create_job_card(work_order, row, qty=0, auto_create=False): def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False):
doc = frappe.new_doc("Job Card") doc = frappe.new_doc("Job Card")
doc.update({ doc.update({
'work_order': work_order.name, 'work_order': work_order.name,
@ -741,6 +781,9 @@ def create_job_card(work_order, row, qty=0, auto_create=False):
if auto_create: if auto_create:
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
if enable_capacity_planning:
doc.schedule_time_logs(row)
doc.insert() doc.insert()
frappe.msgprint(_("Job card {0} created").format(doc.name)) frappe.msgprint(_("Job card {0} created").format(doc.name))

View File

@ -1,466 +1,159 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:workstation_name", "autoname": "field:workstation_name",
"beta": 0,
"creation": "2013-01-10 16:34:17", "creation": "2013-01-10 16:34:17",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0, "field_order": [
"workstation_name",
"production_capacity",
"column_break_3",
"over_heads",
"hour_rate_electricity",
"hour_rate_consumable",
"column_break_11",
"hour_rate_rent",
"hour_rate_labour",
"hour_rate",
"working_hours_section",
"holiday_list",
"working_hours",
"workstaion_description",
"description"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "description_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "workstation_name", "fieldname": "workstation_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Workstation Name", "label": "Workstation Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "workstation_name", "oldfieldname": "workstation_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "unique": 1
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text", "fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Description", "label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Text", "oldfieldtype": "Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "300px" "width": "300px"
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "over_heads", "fieldname": "over_heads",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Operating Costs", "label": "Operating Costs",
"length": 0, "oldfieldtype": "Section Break"
"no_copy": 0,
"oldfieldtype": "Section Break",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "per hour", "description": "per hour",
"fieldname": "hour_rate_electricity", "fieldname": "hour_rate_electricity",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Electricity Cost", "label": "Electricity Cost",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate_electricity", "oldfieldname": "hour_rate_electricity",
"oldfieldtype": "Currency", "oldfieldtype": "Currency"
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "per hour", "description": "per hour",
"fieldname": "hour_rate_consumable", "fieldname": "hour_rate_consumable",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Consumable Cost", "label": "Consumable Cost",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate_consumable", "oldfieldname": "hour_rate_consumable",
"oldfieldtype": "Currency", "oldfieldtype": "Currency"
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "per hour", "description": "per hour",
"fieldname": "hour_rate_rent", "fieldname": "hour_rate_rent",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rent Cost", "label": "Rent Cost",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate_rent", "oldfieldname": "hour_rate_rent",
"oldfieldtype": "Currency", "oldfieldtype": "Currency"
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Wages per hour", "description": "Wages per hour",
"fieldname": "hour_rate_labour", "fieldname": "hour_rate_labour",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Wages", "label": "Wages",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate_labour", "oldfieldname": "hour_rate_labour",
"oldfieldtype": "Currency", "oldfieldtype": "Currency"
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "per hour", "description": "per hour",
"fieldname": "hour_rate", "fieldname": "hour_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Net Hour Rate", "label": "Net Hour Rate",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate", "oldfieldname": "hour_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"permlevel": 0, "read_only": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "working_hours_section", "fieldname": "working_hours_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Working Hours"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Working Hours",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "working_hours", "fieldname": "working_hours",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Working Hours", "label": "Working Hours",
"length": 0, "options": "Workstation Working Hour"
"no_copy": 0,
"options": "Workstation Working Hour",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "holiday_list", "fieldname": "holiday_list",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holiday List", "label": "Holiday List",
"length": 0, "options": "Holiday List"
"no_copy": 0, },
"options": "Holiday List", {
"permlevel": 0, "default": "1",
"precision": "", "fieldname": "production_capacity",
"print_hide": 0, "fieldtype": "Int",
"print_hide_if_no_value": 0, "label": "Production Capacity",
"read_only": 0, "reqd": 1
"remember_last_selected_value": 0, },
"report_hide": 0, {
"reqd": 0, "fieldname": "column_break_3",
"search_index": 0, "fieldtype": "Column Break"
"set_only_once": 0, },
"unique": 0 {
"collapsible": 1,
"fieldname": "workstaion_description",
"fieldtype": "Section Break",
"label": "Description"
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-wrench", "icon": "icon-wrench",
"idx": 1, "idx": 1,
"image_view": 0, "modified": "2019-11-26 12:39:19.742052",
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-18 22:28:50.163219",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Workstation", "name": "Workstation",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Manufacturing User", "role": "Manufacturing User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_order": "ASC", "sort_order": "ASC",
"track_changes": 1, "track_changes": 1
"track_seen": 0
} }

View File

@ -4,7 +4,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt, cint, getdate, formatdate, comma_and, time_diff_in_seconds, to_timedelta from erpnext.support.doctype.issue.issue import get_holidays
from frappe.utils import (flt, cint, getdate, formatdate,
comma_and, time_diff_in_seconds, to_timedelta, add_days)
from frappe.model.document import Document from frappe.model.document import Document
from dateutil.parser import parse from dateutil.parser import parse
@ -43,6 +45,17 @@ class Workstation(Document):
where parent = %s and workstation = %s""", where parent = %s and workstation = %s""",
(self.hour_rate, bom_no[0], self.name)) (self.hour_rate, bom_no[0], self.name))
def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False):
if not skip_holiday_list_check and (not self.holiday_list or
cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"))):
return schedule_date
if schedule_date in tuple(get_holidays(self.holiday_list)):
schedule_date = add_days(schedule_date, 1)
self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
return schedule_date
@frappe.whitelist() @frappe.whitelist()
def get_default_holiday_list(): def get_default_holiday_list():
return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list") return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list")

View File

@ -6,8 +6,12 @@ def get_data():
'fieldname': 'workstation', 'fieldname': 'workstation',
'transactions': [ 'transactions': [
{ {
'label': _('Manufacture'), 'label': _('Master'),
'items': ['BOM', 'Routing', 'Work Order', 'Job Card', 'Operation', 'Timesheet'] 'items': ['BOM', 'Routing', 'Operation']
},
{
'label': _('Transaction'),
'items': ['Work Order', 'Job Card', 'Timesheet']
} }
] ]
} }

View File

@ -14,7 +14,7 @@ def execute(filters=None):
def get_data(filters, data): def get_data(filters, data):
get_exploded_items(filters.bom, data) get_exploded_items(filters.bom, data)
def get_exploded_items(bom, data, indent=0): def get_exploded_items(bom, data, indent=0, qty=1):
exploded_items = frappe.get_all("BOM Item", exploded_items = frappe.get_all("BOM Item",
filters={"parent": bom}, filters={"parent": bom},
fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom']) fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom'])
@ -26,13 +26,13 @@ def get_exploded_items(bom, data, indent=0):
'item_name': item.item_name, 'item_name': item.item_name,
'indent': indent, 'indent': indent,
'bom': item.bom_no, 'bom': item.bom_no,
'qty': item.qty, 'qty': item.qty * qty,
'uom': item.uom, 'uom': item.uom,
'description': item.description, 'description': item.description,
'scrap': item.scrap 'scrap': item.scrap
}) })
if item.bom_no: if item.bom_no:
get_exploded_items(item.bom_no, data, indent=indent+1) get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty)
def get_columns(): def get_columns():
return [ return [

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils.data import comma_and
def execute(filters=None): def execute(filters=None):
# if not filters: filters = {} # if not filters: filters = {}
@ -13,35 +14,36 @@ def execute(filters=None):
data = get_bom_stock(filters) data = get_bom_stock(filters)
qty_to_make = filters.get("qty_to_make") qty_to_make = filters.get("qty_to_make")
manufacture_details = get_manufacturer_records()
for row in data: for row in data:
item_map = get_item_details(row.item_code)
reqd_qty = qty_to_make * row.actual_qty reqd_qty = qty_to_make * row.actual_qty
last_pur_price = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") last_pur_price = frappe.db.get_value("Item", row.item_code, "last_purchase_rate")
if row.to_build > 0:
diff_qty = row.to_build - reqd_qty
summ_data.append([row.item_code, row.description, item_map[row.item_code]["manufacturer"], item_map[row.item_code]["manufacturer_part_no"], row.actual_qty, row.to_build, reqd_qty, diff_qty, last_pur_price])
else:
diff_qty = 0 - reqd_qty
summ_data.append([row.item_code, row.description, item_map[row.item_code]["manufacturer"], item_map[row.item_code]["manufacturer_part_no"], row.actual_qty, "0.000", reqd_qty, diff_qty, last_pur_price])
summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details))
return columns, summ_data return columns, summ_data
def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
to_build = row.to_build if row.to_build > 0 else 0
diff_qty = to_build - reqd_qty
return [row.item_code, row.description,
comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer', []), add_quotes=False),
comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer_part', []), add_quotes=False),
row.actual_qty, str(to_build),
reqd_qty, diff_qty, last_pur_price]
def get_columns(): def get_columns():
"""return columns""" """return columns"""
columns = [ columns = [
_("Item") + ":Link/Item:100", _("Item") + ":Link/Item:100",
_("Description") + "::150", _("Description") + "::150",
_("Manufacturer") + "::100", _("Manufacturer") + "::250",
_("Manufacturer Part Number") + "::100", _("Manufacturer Part Number") + "::250",
_("Qty") + ":Float:50", _("Qty") + ":Float:50",
_("Stock Qty") + ":Float:100", _("Stock Qty") + ":Float:100",
_("Reqd Qty")+ ":Float:100", _("Reqd Qty")+ ":Float:100",
_("Diff Qty")+ ":Float:100", _("Diff Qty")+ ":Float:100",
_("Last Purchase Price")+ ":Float:100", _("Last Purchase Price")+ ":Float:100",
] ]
return columns return columns
def get_bom_stock(filters): def get_bom_stock(filters):
@ -85,7 +87,12 @@ def get_bom_stock(filters):
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_item_details(item_code): def get_manufacturer_records():
items = frappe.db.sql("""select it.item_group, it.item_name, it.stock_uom, it.name, it.brand, it.description, it.manufacturer_part_no, it.manufacturer from tabItem it where it.item_code = %s""", item_code, as_dict=1) details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no, parent"])
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {})
dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
return dict((d.name, d) for d in items) return manufacture_details

View File

@ -647,4 +647,5 @@ erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields
erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template
erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger
erpnext.patches.v12_0.update_price_or_product_discount erpnext.patches.v12_0.update_price_or_product_discount
erpnext.patches.v12_0.set_production_capacity_in_workstation
erpnext.patches.v12_0.set_lead_title_field erpnext.patches.v12_0.set_lead_title_field

View File

@ -62,12 +62,12 @@ def execute():
] ]
for dt in doctypes: for dt in doctypes:
for d in frappe.db.sql("""select name, parent, item_code, item_tax_rate from `tab{0} Item` for d in frappe.db.sql("""select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item`
where ifnull(item_tax_rate, '') not in ('', '{{}}') where ifnull(item_tax_rate, '') not in ('', '{{}}')
and item_tax_template is NULL""".format(dt), as_dict=1): and item_tax_template is NULL""".format(dt), as_dict=1):
item_tax_map = json.loads(d.item_tax_rate) item_tax_map = json.loads(d.item_tax_rate)
item_tax_template_name = get_item_tax_template(item_tax_templates, item_tax_template_name = get_item_tax_template(item_tax_templates,
item_tax_map, d.item_code, d.parent) item_tax_map, d.item_code, d.parenttype, d.parent)
frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name) frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name)
frappe.db.auto_commit_on_many_writes = False frappe.db.auto_commit_on_many_writes = False
@ -77,7 +77,7 @@ def execute():
settings.determine_address_tax_category_from = "Billing Address" settings.determine_address_tax_category_from = "Billing Address"
settings.save() settings.save()
def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=None): def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None):
# search for previously created item tax template by comparing tax maps # search for previously created item tax template by comparing tax maps
for template, item_tax_template_map in iteritems(item_tax_templates): for template, item_tax_template_map in iteritems(item_tax_templates):
if item_tax_map == item_tax_template_map: if item_tax_map == item_tax_template_map:
@ -88,23 +88,44 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=No
item_tax_template.title = make_autoname("Item Tax Template-.####") item_tax_template.title = make_autoname("Item Tax Template-.####")
for tax_type, tax_rate in iteritems(item_tax_map): for tax_type, tax_rate in iteritems(item_tax_map):
if not frappe.db.exists("Account", tax_type): account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type'], as_dict=1)
if account_details:
if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'):
frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable')
else:
parts = tax_type.strip().split(" - ") parts = tax_type.strip().split(" - ")
account_name = " - ".join(parts[:-1]) account_name = " - ".join(parts[:-1])
company = frappe.db.get_value("Company", filters={"abbr": parts[-1]}) company = get_company(parts[-1], parenttype, parent)
parent_account = frappe.db.get_value("Account", parent_account = frappe.db.get_value("Account",
filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account") filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account")
filters = {
frappe.get_doc({
"doctype": "Account",
"account_name": account_name, "account_name": account_name,
"company": company, "company": company,
"account_type": "Tax", "account_type": "Tax",
"parent_account": parent_account "parent_account": parent_account
}).insert() }
tax_type = frappe.db.get_value("Account", filters)
if not tax_type:
account = frappe.new_doc("Account")
account.update(filters)
account.insert()
tax_type = account.name
item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate}) item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate})
item_tax_templates.setdefault(item_tax_template.title, {}) item_tax_templates.setdefault(item_tax_template.title, {})
item_tax_templates[item_tax_template.title][tax_type] = tax_rate item_tax_templates[item_tax_template.title][tax_type] = tax_rate
item_tax_template.save() item_tax_template.save()
return item_tax_template.name return item_tax_template.name
def get_company(company_abbr, parenttype=None, parent=None):
if parenttype and parent:
company = frappe.get_cached_value(parenttype, parent, 'company')
else:
company = frappe.db.get_value("Company", filters={"abbr": company_abbr})
if not company:
companies = frappe.get_all('Company')
if len(companies) == 1:
company = companies[0].name
return company

View File

@ -0,0 +1,8 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("manufacturing", "doctype", "workstation")
frappe.db.sql(""" UPDATE `tabWorkstation`
SET production_capacity = 1 """)

View File

@ -5,6 +5,9 @@ from frappe import _
def execute(): def execute():
"""Add setup progress actions""" """Add setup progress actions"""
if not frappe.db.exists('DocType', 'Setup Progress') or not frappe.db.exists('DocType', 'Setup Progress Action'):
return
frappe.reload_doc("setup", "doctype", "setup_progress") frappe.reload_doc("setup", "doctype", "setup_progress")
frappe.reload_doc("setup", "doctype", "setup_progress_action") frappe.reload_doc("setup", "doctype", "setup_progress_action")

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.tests.test_website import set_request from frappe.utils import set_request
from frappe.website.render import render from frappe.website.render import render
class TestHomepage(unittest.TestCase): class TestHomepage(unittest.TestCase):

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from frappe.tests.test_website import set_request from frappe.utils import set_request
from frappe.website.render import render from frappe.website.render import render
class TestHomepageSection(unittest.TestCase): class TestHomepageSection(unittest.TestCase):

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import frappe, unittest import frappe, unittest
from frappe.tests.test_website import set_request, get_html_for_route from frappe.utils import set_request, get_html_for_route
from frappe.website.render import render from frappe.website.render import render
from erpnext.portal.product_configurator.utils import get_products_for_website from erpnext.portal.product_configurator.utils import get_products_for_website
from erpnext.stock.doctype.item.test_item import make_item_variant from erpnext.stock.doctype.item.test_item import make_item_variant

View File

@ -313,13 +313,25 @@ def get_items(filters=None, search=None):
search_condition = '' search_condition = ''
if search: if search:
# Default fields to search from
default_fields = {'name', 'item_name', 'description', 'item_group'}
# Get meta search fields
meta = frappe.get_meta("Item")
meta_fields = set(meta.get_search_fields())
# Join the meta fields and default fields set
search_fields = default_fields.union(meta_fields)
try:
if frappe.db.count('Item', cache=True) > 50000:
search_fields.remove('description')
except KeyError:
pass
# Build or filters for query
search = '%{}%'.format(search) search = '%{}%'.format(search)
or_filters = [ or_filters = [[field, 'like', search] for field in search_fields]
['name', 'like', search],
['item_name', 'like', search],
['description', 'like', search],
['item_group', 'like', search]
]
search_condition = get_conditions(or_filters, 'or') search_condition = get_conditions(or_filters, 'or')
filter_condition = get_conditions(filters, 'and') filter_condition = get_conditions(filters, 'and')

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -1716,6 +1716,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}, },
against_blanket_order: function(doc, cdt, cdn) {
var item = locals[cdt][cdn];
if(!item.against_blanket_order) {
frappe.model.set_value(this.frm.doctype + " Item", item.name, "blanket_order", null);
frappe.model.set_value(this.frm.doctype + " Item", item.name, "blanket_order_rate", 0.00);
}
},
blanket_order: function(doc, cdt, cdn) { blanket_order: function(doc, cdt, cdn) {
var me = this; var me = this;
var item = locals[cdt][cdn]; var item = locals[cdt][cdn];

View File

@ -3,6 +3,26 @@
frappe.ui.form.on('GST HSN Code', { frappe.ui.form.on('GST HSN Code', {
refresh: function(frm) { refresh: function(frm) {
if(! frm.doc.__islocal && frm.doc.taxes.length){
frm.add_custom_button(__('Update Taxes for Items'), function(){
frappe.confirm(
'Are you sure? It will overwrite taxes for all items with HSN Code <b>'+frm.doc.name+'</b>.',
function(){
frappe.call({
args:{
taxes: frm.doc.taxes,
hsn_code: frm.doc.name
},
method: 'erpnext.regional.doctype.gst_hsn_code.gst_hsn_code.update_taxes_in_item_master',
callback: function(r) {
if(r.message){
frappe.show_alert(__('Item taxes updated'));
}
}
});
}
);
});
}
} }
}); });

View File

@ -1,104 +1,46 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:hsn_code", "autoname": "field:hsn_code",
"beta": 0,
"creation": "2017-06-21 10:48:56.422086", "creation": "2017-06-21 10:48:56.422086",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"hsn_code",
"description",
"taxes"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "hsn_code", "fieldname": "hsn_code",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "HSN Code", "label": "HSN Code",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "unique": 1
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Description"
"label": "Description", },
"length": 0, {
"no_copy": 0, "fieldname": "taxes",
"permlevel": 0, "fieldtype": "Table",
"precision": "", "label": "Taxes",
"print_hide": 0, "options": "Item Tax"
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "modified": "2019-11-01 11:18:59.556931",
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-09-29 14:38:52.220743",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "GST HSN Code", "name": "GST HSN Code",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "hsn_code, description", "search_fields": "hsn_code, description",
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "hsn_code", "title_field": "hsn_code",
"track_changes": 1, "track_changes": 1
"track_seen": 0
} }

View File

@ -8,3 +8,22 @@ from frappe.model.document import Document
class GSTHSNCode(Document): class GSTHSNCode(Document):
pass pass
@frappe.whitelist()
def update_taxes_in_item_master(taxes, hsn_code):
items = frappe.get_list("Item", filters={
'gst_hsn_code': hsn_code
})
taxes = frappe.parse_json(taxes)
frappe.enqueue(update_item_document, items=items, taxes=taxes)
return 1
def update_item_document(items, taxes):
for item in items:
item_to_be_updated=frappe.get_doc("Item", item.name)
item_to_be_updated.taxes = []
for tax in taxes:
tax = frappe._dict(tax)
item_to_be_updated.append("taxes", {'item_tax_template': tax.item_tax_template, 'tax_category': tax.tax_category})
item_to_be_updated.save()

View File

@ -10,17 +10,26 @@ Provide a report and downloadable CSV according to the German DATEV format.
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
import json import json
import zlib
import zipfile
import six
from six import BytesIO
from six import string_types from six import string_types
import frappe import frappe
from frappe import _ from frappe import _
import pandas as pd import pandas as pd
from .datev_constants import DataCategory
from .datev_constants import Transactions
from .datev_constants import DebtorsCreditors
from .datev_constants import AccountNames
from .datev_constants import QUERY_REPORT_COLUMNS
def execute(filters=None): def execute(filters=None):
"""Entry point for frappe.""" """Entry point for frappe."""
validate(filters) validate(filters)
result = get_gl_entries(filters, as_dict=0) result = get_transactions(filters, as_dict=0)
columns = get_columns() columns = QUERY_REPORT_COLUMNS
return columns, result return columns, result
@ -41,65 +50,8 @@ def validate(filters):
except frappe.DoesNotExistError: except frappe.DoesNotExistError:
frappe.throw(_('Please create <b>DATEV Settings</b> for Company <b>{}</b>.').format(filters.get('company'))) frappe.throw(_('Please create <b>DATEV Settings</b> for Company <b>{}</b>.').format(filters.get('company')))
def get_columns():
"""Return the list of columns that will be shown in query report."""
columns = [
{
"label": "Umsatz (ohne Soll/Haben-Kz)",
"fieldname": "Umsatz (ohne Soll/Haben-Kz)",
"fieldtype": "Currency",
},
{
"label": "Soll/Haben-Kennzeichen",
"fieldname": "Soll/Haben-Kennzeichen",
"fieldtype": "Data",
},
{
"label": "Kontonummer",
"fieldname": "Kontonummer",
"fieldtype": "Data",
},
{
"label": "Gegenkonto (ohne BU-Schlüssel)",
"fieldname": "Gegenkonto (ohne BU-Schlüssel)",
"fieldtype": "Data",
},
{
"label": "Belegdatum",
"fieldname": "Belegdatum",
"fieldtype": "Date",
},
{
"label": "Buchungstext",
"fieldname": "Buchungstext",
"fieldtype": "Text",
},
{
"label": "Beleginfo - Art 1",
"fieldname": "Beleginfo - Art 1",
"fieldtype": "Data",
},
{
"label": "Beleginfo - Inhalt 1",
"fieldname": "Beleginfo - Inhalt 1",
"fieldtype": "Data",
},
{
"label": "Beleginfo - Art 2",
"fieldname": "Beleginfo - Art 2",
"fieldtype": "Data",
},
{
"label": "Beleginfo - Inhalt 2",
"fieldname": "Beleginfo - Inhalt 2",
"fieldtype": "Data",
}
]
return columns def get_transactions(filters, as_dict=1):
def get_gl_entries(filters, as_dict):
""" """
Get a list of accounting entries. Get a list of accounting entries.
@ -111,7 +63,7 @@ def get_gl_entries(filters, as_dict):
as_dict -- return as list of dicts [0,1] as_dict -- return as list of dicts [0,1]
""" """
gl_entries = frappe.db.sql(""" gl_entries = frappe.db.sql("""
select SELECT
/* either debit or credit amount; always positive */ /* either debit or credit amount; always positive */
case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)', case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)',
@ -132,7 +84,7 @@ def get_gl_entries(filters, as_dict):
gl.against_voucher_type as 'Beleginfo - Art 2', gl.against_voucher_type as 'Beleginfo - Art 2',
gl.against_voucher as 'Beleginfo - Inhalt 2' gl.against_voucher as 'Beleginfo - Inhalt 2'
from `tabGL Entry` gl FROM `tabGL Entry` gl
/* Statistisches Konto (Debitoren/Kreditoren) */ /* Statistisches Konto (Debitoren/Kreditoren) */
left join `tabParty Account` pa left join `tabParty Account` pa
@ -155,15 +107,127 @@ def get_gl_entries(filters, as_dict):
left join `tabAccount` acc_against_pa left join `tabAccount` acc_against_pa
on pa.account = acc_against_pa.name on pa.account = acc_against_pa.name
where gl.company = %(company)s WHERE gl.company = %(company)s
and DATE(gl.posting_date) >= %(from_date)s AND DATE(gl.posting_date) >= %(from_date)s
and DATE(gl.posting_date) <= %(to_date)s AND DATE(gl.posting_date) <= %(to_date)s
order by 'Belegdatum', gl.voucher_no""", filters, as_dict=as_dict) ORDER BY 'Belegdatum', gl.voucher_no""", filters, as_dict=as_dict, as_utf8=1)
return gl_entries return gl_entries
def get_datev_csv(data, filters): def get_customers(filters):
"""
Get a list of Customers.
Arguments:
filters -- dict of filters to be passed to the sql query
"""
return frappe.db.sql("""
SELECT
acc.account_number as 'Konto',
cus.customer_name as 'Name (Adressatentyp Unternehmen)',
case cus.customer_type when 'Individual' then 1 when 'Company' then 2 else 0 end as 'Adressatentyp',
adr.address_line1 as 'Straße',
adr.pincode as 'Postleitzahl',
adr.city as 'Ort',
UPPER(country.code) as 'Land',
adr.address_line2 as 'Adresszusatz',
con.email_id as 'E-Mail',
coalesce(con.mobile_no, con.phone) as 'Telefon',
cus.website as 'Internet',
cus.tax_id as 'Steuernummer',
ccl.credit_limit as 'Kreditlimit (Debitor)'
FROM `tabParty Account` par
left join `tabAccount` acc
on acc.name = par.account
left join `tabCustomer` cus
on cus.name = par.parent
left join `tabAddress` adr
on adr.name = cus.customer_primary_address
left join `tabCountry` country
on country.name = adr.country
left join `tabContact` con
on con.name = cus.customer_primary_contact
left join `tabCustomer Credit Limit` ccl
on ccl.parent = cus.name
and ccl.company = par.company
WHERE par.company = %(company)s
AND par.parenttype = 'Customer'""", filters, as_dict=1, as_utf8=1)
def get_suppliers(filters):
"""
Get a list of Suppliers.
Arguments:
filters -- dict of filters to be passed to the sql query
"""
return frappe.db.sql("""
SELECT
acc.account_number as 'Konto',
sup.supplier_name as 'Name (Adressatentyp Unternehmen)',
case sup.supplier_type when 'Individual' then '1' when 'Company' then '2' else '0' end as 'Adressatentyp',
adr.address_line1 as 'Straße',
adr.pincode as 'Postleitzahl',
adr.city as 'Ort',
UPPER(country.code) as 'Land',
adr.address_line2 as 'Adresszusatz',
con.email_id as 'E-Mail',
coalesce(con.mobile_no, con.phone) as 'Telefon',
sup.website as 'Internet',
sup.tax_id as 'Steuernummer',
case sup.on_hold when 1 then sup.release_date else null end as 'Zahlungssperre bis'
FROM `tabParty Account` par
left join `tabAccount` acc
on acc.name = par.account
left join `tabSupplier` sup
on sup.name = par.parent
left join `tabDynamic Link` dyn_adr
on dyn_adr.link_name = sup.name
and dyn_adr.link_doctype = 'Supplier'
and dyn_adr.parenttype = 'Address'
left join `tabAddress` adr
on adr.name = dyn_adr.parent
and adr.is_primary_address = '1'
left join `tabCountry` country
on country.name = adr.country
left join `tabDynamic Link` dyn_con
on dyn_con.link_name = sup.name
and dyn_con.link_doctype = 'Supplier'
and dyn_con.parenttype = 'Contact'
left join `tabContact` con
on con.name = dyn_con.parent
and con.is_primary_contact = '1'
WHERE par.company = %(company)s
AND par.parenttype = 'Supplier'""", filters, as_dict=1, as_utf8=1)
def get_account_names(filters):
return frappe.get_list("Account",
fields=["account_number as Konto", "name as Kontenbeschriftung"],
filters={"company": filters.get("company"), "is_group": "0"})
def get_datev_csv(data, filters, csv_class):
""" """
Fill in missing columns and return a CSV in DATEV Format. Fill in missing columns and return a CSV in DATEV Format.
@ -174,7 +238,46 @@ def get_datev_csv(data, filters):
Arguments: Arguments:
data -- array of dictionaries data -- array of dictionaries
filters -- dict filters -- dict
csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS
""" """
header = get_header(filters, csv_class)
empty_df = pd.DataFrame(columns=csv_class.COLUMNS)
data_df = pd.DataFrame.from_records(data)
result = empty_df.append(data_df, sort=True)
if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS:
result['Belegdatum'] = pd.to_datetime(result['Belegdatum'])
if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES:
result['Sprach-ID'] = 'de-DE'
header = ';'.join(header).encode('latin_1')
data = result.to_csv(
# Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035
sep=str(';'),
# European decimal seperator
decimal=',',
# Windows "ANSI" encoding
encoding='latin_1',
# format date as DDMM
date_format='%d%m',
# Windows line terminator
line_terminator='\r\n',
# Do not number rows
index=False,
# Use all columns defined above
columns=csv_class.COLUMNS
)
if not six.PY2:
data = data.encode('latin_1')
return header + b'\r\n' + data
def get_header(filters, csv_class):
header = [ header = [
# A = DATEV format # A = DATEV format
# DTVF = created by DATEV software, # DTVF = created by DATEV software,
@ -185,18 +288,8 @@ def get_datev_csv(data, filters):
# 510 = 5.10, # 510 = 5.10,
# 720 = 7.20 # 720 = 7.20
"510", "510",
# C = Data category csv_class.DATA_CATEGORY,
# 21 = Transaction batch (Buchungsstapel), csv_class.FORMAT_NAME,
# 67 = Buchungstextkonstanten,
# 16 = Debitors/Creditors,
# 20 = Account names (Kontenbeschriftungen)
"21",
# D = Format name
# Buchungsstapel,
# Buchungstextkonstanten,
# Debitoren/Kreditoren,
# Kontenbeschriftungen
"Buchungsstapel",
# E = Format version (regarding format name) # E = Format version (regarding format name)
"", "",
# F = Generated on # F = Generated on
@ -224,16 +317,17 @@ def get_datev_csv(data, filters):
# P = Transaction batch end date (YYYYMMDD) # P = Transaction batch end date (YYYYMMDD)
frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd"), frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd"),
# Q = Description (for example, "January - February 2019 Transactions") # Q = Description (for example, "January - February 2019 Transactions")
"{} - {} Buchungsstapel".format( "{} - {} {}".format(
frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"), frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"),
frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy") frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy"),
csv_class.FORMAT_NAME
), ),
# R = Diktatkürzel # R = Diktatkürzel
"", "",
# S = Buchungstyp # S = Buchungstyp
# 1 = Transaction batch (Buchungsstapel), # 1 = Transaction batch (Buchungsstapel),
# 2 = Annual financial statement (Jahresabschluss) # 2 = Annual financial statement (Jahresabschluss)
"1", "1" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "",
# T = Rechnungslegungszweck # T = Rechnungslegungszweck
"", "",
# U = Festschreibung # U = Festschreibung
@ -241,185 +335,8 @@ def get_datev_csv(data, filters):
# V = Kontoführungs-Währungskennzeichen des Geldkontos # V = Kontoführungs-Währungskennzeichen des Geldkontos
frappe.get_value("Company", filters.get("company"), "default_currency") frappe.get_value("Company", filters.get("company"), "default_currency")
] ]
columns = [ return header
# All possible columns must tbe listed here, because DATEV requires them to
# be present in the CSV.
# ---
# Umsatz
"Umsatz (ohne Soll/Haben-Kz)",
"Soll/Haben-Kennzeichen",
"WKZ Umsatz",
"Kurs",
"Basis-Umsatz",
"WKZ Basis-Umsatz",
# Konto/Gegenkonto
"Kontonummer",
"Gegenkonto (ohne BU-Schlüssel)",
"BU-Schlüssel",
# Datum
"Belegdatum",
# Belegfelder
"Belegfeld 1",
"Belegfeld 2",
# Weitere Felder
"Skonto",
"Buchungstext",
# OPOS-Informationen
"Postensperre",
"Diverse Adressnummer",
"Geschäftspartnerbank",
"Sachverhalt",
"Zinssperre",
# Digitaler Beleg
"Beleglink",
# Beleginfo
"Beleginfo - Art 1",
"Beleginfo - Inhalt 1",
"Beleginfo - Art 2",
"Beleginfo - Inhalt 2",
"Beleginfo - Art 3",
"Beleginfo - Inhalt 3",
"Beleginfo - Art 4",
"Beleginfo - Inhalt 4",
"Beleginfo - Art 5",
"Beleginfo - Inhalt 5",
"Beleginfo - Art 6",
"Beleginfo - Inhalt 6",
"Beleginfo - Art 7",
"Beleginfo - Inhalt 7",
"Beleginfo - Art 8",
"Beleginfo - Inhalt 8",
# Kostenrechnung
"Kost 1 - Kostenstelle",
"Kost 2 - Kostenstelle",
"Kost-Menge",
# Steuerrechnung
"EU-Land u. UStID",
"EU-Steuersatz",
"Abw. Versteuerungsart",
# L+L Sachverhalt
"Sachverhalt L+L",
"Funktionsergänzung L+L",
# Funktion Steuerschlüssel 49
"BU 49 Hauptfunktionstyp",
"BU 49 Hauptfunktionsnummer",
"BU 49 Funktionsergänzung",
# Zusatzinformationen
"Zusatzinformation - Art 1",
"Zusatzinformation - Inhalt 1",
"Zusatzinformation - Art 2",
"Zusatzinformation - Inhalt 2",
"Zusatzinformation - Art 3",
"Zusatzinformation - Inhalt 3",
"Zusatzinformation - Art 4",
"Zusatzinformation - Inhalt 4",
"Zusatzinformation - Art 5",
"Zusatzinformation - Inhalt 5",
"Zusatzinformation - Art 6",
"Zusatzinformation - Inhalt 6",
"Zusatzinformation - Art 7",
"Zusatzinformation - Inhalt 7",
"Zusatzinformation - Art 8",
"Zusatzinformation - Inhalt 8",
"Zusatzinformation - Art 9",
"Zusatzinformation - Inhalt 9",
"Zusatzinformation - Art 10",
"Zusatzinformation - Inhalt 10",
"Zusatzinformation - Art 11",
"Zusatzinformation - Inhalt 11",
"Zusatzinformation - Art 12",
"Zusatzinformation - Inhalt 12",
"Zusatzinformation - Art 13",
"Zusatzinformation - Inhalt 13",
"Zusatzinformation - Art 14",
"Zusatzinformation - Inhalt 14",
"Zusatzinformation - Art 15",
"Zusatzinformation - Inhalt 15",
"Zusatzinformation - Art 16",
"Zusatzinformation - Inhalt 16",
"Zusatzinformation - Art 17",
"Zusatzinformation - Inhalt 17",
"Zusatzinformation - Art 18",
"Zusatzinformation - Inhalt 18",
"Zusatzinformation - Art 19",
"Zusatzinformation - Inhalt 19",
"Zusatzinformation - Art 20",
"Zusatzinformation - Inhalt 20",
# Mengenfelder LuF
"Stück",
"Gewicht",
# Forderungsart
"Zahlweise",
"Forderungsart",
"Veranlagungsjahr",
"Zugeordnete Fälligkeit",
# Weitere Felder
"Skontotyp",
# Anzahlungen
"Auftragsnummer",
"Buchungstyp",
"USt-Schlüssel (Anzahlungen)",
"EU-Land (Anzahlungen)",
"Sachverhalt L+L (Anzahlungen)",
"EU-Steuersatz (Anzahlungen)",
"Erlöskonto (Anzahlungen)",
# Stapelinformationen
"Herkunft-Kz",
# Technische Identifikation
"Buchungs GUID",
# Kostenrechnung
"Kost-Datum",
# OPOS-Informationen
"SEPA-Mandatsreferenz",
"Skontosperre",
# Gesellschafter und Sonderbilanzsachverhalt
"Gesellschaftername",
"Beteiligtennummer",
"Identifikationsnummer",
"Zeichnernummer",
# OPOS-Informationen
"Postensperre bis",
# Gesellschafter und Sonderbilanzsachverhalt
"Bezeichnung SoBil-Sachverhalt",
"Kennzeichen SoBil-Buchung",
# Stapelinformationen
"Festschreibung",
# Datum
"Leistungsdatum",
"Datum Zuord. Steuerperiode",
# OPOS-Informationen
"Fälligkeit",
# Konto/Gegenkonto
"Generalumkehr (GU)",
# Steuersatz für Steuerschlüssel
"Steuersatz",
"Land"
]
empty_df = pd.DataFrame(columns=columns)
data_df = pd.DataFrame.from_records(data)
result = empty_df.append(data_df)
result['Belegdatum'] = pd.to_datetime(result['Belegdatum'])
header = ';'.join(header).encode('latin_1')
data = result.to_csv(
sep=b';',
# European decimal seperator
decimal=',',
# Windows "ANSI" encoding
encoding='latin_1',
# format date as DDMM
date_format='%d%m',
# Windows line terminator
line_terminator=b'\r\n',
# Do not number rows
index=False,
# Use all columns defined above
columns=columns
)
return header + b'\r\n' + data
@frappe.whitelist() @frappe.whitelist()
def download_datev_csv(filters=None): def download_datev_csv(filters=None):
@ -438,8 +355,31 @@ def download_datev_csv(filters=None):
filters = json.loads(filters) filters = json.loads(filters)
validate(filters) validate(filters)
data = get_gl_entries(filters, as_dict=1)
frappe.response['result'] = get_datev_csv(data, filters) # This is where my zip will be written
frappe.response['doctype'] = 'EXTF_Buchungsstapel' zip_buffer = BytesIO()
frappe.response['type'] = 'csv' # This is my zip file
datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
transactions = get_transactions(filters)
transactions_csv = get_datev_csv(transactions, filters, csv_class=Transactions)
datev_zip.writestr('EXTF_Buchungsstapel.csv', transactions_csv)
account_names = get_account_names(filters)
account_names_csv = get_datev_csv(account_names, filters, csv_class=AccountNames)
datev_zip.writestr('EXTF_Kontenbeschriftungen.csv', account_names_csv)
customers = get_customers(filters)
customers_csv = get_datev_csv(customers, filters, csv_class=DebtorsCreditors)
datev_zip.writestr('EXTF_Kunden.csv', customers_csv)
suppliers = get_suppliers(filters)
suppliers_csv = get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors)
datev_zip.writestr('EXTF_Lieferanten.csv', suppliers_csv)
# You must call close() before exiting your program or essential records will not be written.
datev_zip.close()
frappe.response['filecontent'] = zip_buffer.getvalue()
frappe.response['filename'] = 'DATEV.zip'
frappe.response['type'] = 'binary'

View File

@ -0,0 +1,512 @@
# coding: utf-8
"""Constants used in datev.py."""
TRANSACTION_COLUMNS = [
# All possible columns must tbe listed here, because DATEV requires them to
# be present in the CSV.
# ---
# Umsatz
"Umsatz (ohne Soll/Haben-Kz)",
"Soll/Haben-Kennzeichen",
"WKZ Umsatz",
"Kurs",
"Basis-Umsatz",
"WKZ Basis-Umsatz",
# Konto/Gegenkonto
"Kontonummer",
"Gegenkonto (ohne BU-Schlüssel)",
"BU-Schlüssel",
# Datum
"Belegdatum",
# Belegfelder
"Belegfeld 1",
"Belegfeld 2",
# Weitere Felder
"Skonto",
"Buchungstext",
# OPOS-Informationen
"Postensperre",
"Diverse Adressnummer",
"Geschäftspartnerbank",
"Sachverhalt",
"Zinssperre",
# Digitaler Beleg
"Beleglink",
# Beleginfo
"Beleginfo - Art 1",
"Beleginfo - Inhalt 1",
"Beleginfo - Art 2",
"Beleginfo - Inhalt 2",
"Beleginfo - Art 3",
"Beleginfo - Inhalt 3",
"Beleginfo - Art 4",
"Beleginfo - Inhalt 4",
"Beleginfo - Art 5",
"Beleginfo - Inhalt 5",
"Beleginfo - Art 6",
"Beleginfo - Inhalt 6",
"Beleginfo - Art 7",
"Beleginfo - Inhalt 7",
"Beleginfo - Art 8",
"Beleginfo - Inhalt 8",
# Kostenrechnung
"Kost 1 - Kostenstelle",
"Kost 2 - Kostenstelle",
"Kost-Menge",
# Steuerrechnung
"EU-Land u. UStID",
"EU-Steuersatz",
"Abw. Versteuerungsart",
# L+L Sachverhalt
"Sachverhalt L+L",
"Funktionsergänzung L+L",
# Funktion Steuerschlüssel 49
"BU 49 Hauptfunktionstyp",
"BU 49 Hauptfunktionsnummer",
"BU 49 Funktionsergänzung",
# Zusatzinformationen
"Zusatzinformation - Art 1",
"Zusatzinformation - Inhalt 1",
"Zusatzinformation - Art 2",
"Zusatzinformation - Inhalt 2",
"Zusatzinformation - Art 3",
"Zusatzinformation - Inhalt 3",
"Zusatzinformation - Art 4",
"Zusatzinformation - Inhalt 4",
"Zusatzinformation - Art 5",
"Zusatzinformation - Inhalt 5",
"Zusatzinformation - Art 6",
"Zusatzinformation - Inhalt 6",
"Zusatzinformation - Art 7",
"Zusatzinformation - Inhalt 7",
"Zusatzinformation - Art 8",
"Zusatzinformation - Inhalt 8",
"Zusatzinformation - Art 9",
"Zusatzinformation - Inhalt 9",
"Zusatzinformation - Art 10",
"Zusatzinformation - Inhalt 10",
"Zusatzinformation - Art 11",
"Zusatzinformation - Inhalt 11",
"Zusatzinformation - Art 12",
"Zusatzinformation - Inhalt 12",
"Zusatzinformation - Art 13",
"Zusatzinformation - Inhalt 13",
"Zusatzinformation - Art 14",
"Zusatzinformation - Inhalt 14",
"Zusatzinformation - Art 15",
"Zusatzinformation - Inhalt 15",
"Zusatzinformation - Art 16",
"Zusatzinformation - Inhalt 16",
"Zusatzinformation - Art 17",
"Zusatzinformation - Inhalt 17",
"Zusatzinformation - Art 18",
"Zusatzinformation - Inhalt 18",
"Zusatzinformation - Art 19",
"Zusatzinformation - Inhalt 19",
"Zusatzinformation - Art 20",
"Zusatzinformation - Inhalt 20",
# Mengenfelder LuF
"Stück",
"Gewicht",
# Forderungsart
"Zahlweise",
"Forderungsart",
"Veranlagungsjahr",
"Zugeordnete Fälligkeit",
# Weitere Felder
"Skontotyp",
# Anzahlungen
"Auftragsnummer",
"Buchungstyp",
"USt-Schlüssel (Anzahlungen)",
"EU-Land (Anzahlungen)",
"Sachverhalt L+L (Anzahlungen)",
"EU-Steuersatz (Anzahlungen)",
"Erlöskonto (Anzahlungen)",
# Stapelinformationen
"Herkunft-Kz",
# Technische Identifikation
"Buchungs GUID",
# Kostenrechnung
"Kost-Datum",
# OPOS-Informationen
"SEPA-Mandatsreferenz",
"Skontosperre",
# Gesellschafter und Sonderbilanzsachverhalt
"Gesellschaftername",
"Beteiligtennummer",
"Identifikationsnummer",
"Zeichnernummer",
# OPOS-Informationen
"Postensperre bis",
# Gesellschafter und Sonderbilanzsachverhalt
"Bezeichnung SoBil-Sachverhalt",
"Kennzeichen SoBil-Buchung",
# Stapelinformationen
"Festschreibung",
# Datum
"Leistungsdatum",
"Datum Zuord. Steuerperiode",
# OPOS-Informationen
"Fälligkeit",
# Konto/Gegenkonto
"Generalumkehr (GU)",
# Steuersatz für Steuerschlüssel
"Steuersatz",
"Land"
]
DEBTOR_CREDITOR_COLUMNS = [
# All possible columns must tbe listed here, because DATEV requires them to
# be present in the CSV.
# Columns "Leerfeld" have been replaced with "Leerfeld #" to not confuse pandas
# ---
"Konto",
"Name (Adressatentyp Unternehmen)",
"Unternehmensgegenstand",
"Name (Adressatentyp natürl. Person)",
"Vorname (Adressatentyp natürl. Person)",
"Name (Adressatentyp keine Angabe)",
"Adressatentyp",
"Kurzbezeichnung",
"EU-Land",
"EU-USt-IdNr.",
"Anrede",
"Titel/Akad. Grad",
"Adelstitel",
"Namensvorsatz",
"Adressart",
"Straße",
"Postfach",
"Postleitzahl",
"Ort",
"Land",
"Versandzusatz",
"Adresszusatz",
"Abweichende Anrede",
"Abw. Zustellbezeichnung 1",
"Abw. Zustellbezeichnung 2",
"Kennz. Korrespondenzadresse",
"Adresse gültig von",
"Adresse gültig bis",
"Telefon",
"Bemerkung (Telefon)",
"Telefon Geschäftsleitung",
"Bemerkung (Telefon GL)",
"E-Mail",
"Bemerkung (E-Mail)",
"Internet",
"Bemerkung (Internet)",
"Fax",
"Bemerkung (Fax)",
"Sonstige",
"Bemerkung (Sonstige)",
"Bankleitzahl 1",
"Bankbezeichnung 1",
"Bankkonto-Nummer 1",
"Länderkennzeichen 1",
"IBAN 1",
"Leerfeld 1",
"SWIFT-Code 1",
"Abw. Kontoinhaber 1",
"Kennz. Haupt-Bankverb. 1",
"Bankverb. 1 Gültig von",
"Bankverb. 1 Gültig bis",
"Bankleitzahl 2",
"Bankbezeichnung 2",
"Bankkonto-Nummer 2",
"Länderkennzeichen 2",
"IBAN 2",
"Leerfeld 2",
"SWIFT-Code 2",
"Abw. Kontoinhaber 2",
"Kennz. Haupt-Bankverb. 2",
"Bankverb. 2 gültig von",
"Bankverb. 2 gültig bis",
"Bankleitzahl 3",
"Bankbezeichnung 3",
"Bankkonto-Nummer 3",
"Länderkennzeichen 3",
"IBAN 3",
"Leerfeld 3",
"SWIFT-Code 3",
"Abw. Kontoinhaber 3",
"Kennz. Haupt-Bankverb. 3",
"Bankverb. 3 gültig von",
"Bankverb. 3 gültig bis",
"Bankleitzahl 4",
"Bankbezeichnung 4",
"Bankkonto-Nummer 4",
"Länderkennzeichen 4",
"IBAN 4",
"Leerfeld 4",
"SWIFT-Code 4",
"Abw. Kontoinhaber 4",
"Kennz. Haupt-Bankverb. 4",
"Bankverb. 4 Gültig von",
"Bankverb. 4 Gültig bis",
"Bankleitzahl 5",
"Bankbezeichnung 5",
"Bankkonto-Nummer 5",
"Länderkennzeichen 5",
"IBAN 5",
"Leerfeld 5",
"SWIFT-Code 5",
"Abw. Kontoinhaber 5",
"Kennz. Haupt-Bankverb. 5",
"Bankverb. 5 gültig von",
"Bankverb. 5 gültig bis",
"Leerfeld 6",
"Briefanrede",
"Grußformel",
"Kundennummer",
"Steuernummer",
"Sprache",
"Ansprechpartner",
"Vertreter",
"Sachbearbeiter",
"Diverse-Konto",
"Ausgabeziel",
"Währungssteuerung",
"Kreditlimit (Debitor)",
"Zahlungsbedingung",
"Fälligkeit in Tagen (Debitor)",
"Skonto in Prozent (Debitor)",
"Kreditoren-Ziel 1 (Tage)",
"Kreditoren-Skonto 1 (%)",
"Kreditoren-Ziel 2 (Tage)",
"Kreditoren-Skonto 2 (%)",
"Kreditoren-Ziel 3 Brutto (Tage)",
"Kreditoren-Ziel 4 (Tage)",
"Kreditoren-Skonto 4 (%)",
"Kreditoren-Ziel 5 (Tage)",
"Kreditoren-Skonto 5 (%)",
"Mahnung",
"Kontoauszug",
"Mahntext 1",
"Mahntext 2",
"Mahntext 3",
"Kontoauszugstext",
"Mahnlimit Betrag",
"Mahnlimit %",
"Zinsberechnung",
"Mahnzinssatz 1",
"Mahnzinssatz 2",
"Mahnzinssatz 3",
"Lastschrift",
"Verfahren",
"Mandantenbank",
"Zahlungsträger",
"Indiv. Feld 1",
"Indiv. Feld 2",
"Indiv. Feld 3",
"Indiv. Feld 4",
"Indiv. Feld 5",
"Indiv. Feld 6",
"Indiv. Feld 7",
"Indiv. Feld 8",
"Indiv. Feld 9",
"Indiv. Feld 10",
"Indiv. Feld 11",
"Indiv. Feld 12",
"Indiv. Feld 13",
"Indiv. Feld 14",
"Indiv. Feld 15",
"Abweichende Anrede (Rechnungsadresse)",
"Adressart (Rechnungsadresse)",
"Straße (Rechnungsadresse)",
"Postfach (Rechnungsadresse)",
"Postleitzahl (Rechnungsadresse)",
"Ort (Rechnungsadresse)",
"Land (Rechnungsadresse)",
"Versandzusatz (Rechnungsadresse)",
"Adresszusatz (Rechnungsadresse)",
"Abw. Zustellbezeichnung 1 (Rechnungsadresse)",
"Abw. Zustellbezeichnung 2 (Rechnungsadresse)",
"Adresse Gültig von (Rechnungsadresse)",
"Adresse Gültig bis (Rechnungsadresse)",
"Bankleitzahl 6",
"Bankbezeichnung 6",
"Bankkonto-Nummer 6",
"Länderkennzeichen 6",
"IBAN 6",
"Leerfeld 7",
"SWIFT-Code 6",
"Abw. Kontoinhaber 6",
"Kennz. Haupt-Bankverb. 6",
"Bankverb 6 gültig von",
"Bankverb 6 gültig bis",
"Bankleitzahl 7",
"Bankbezeichnung 7",
"Bankkonto-Nummer 7",
"Länderkennzeichen 7",
"IBAN 7",
"Leerfeld 8",
"SWIFT-Code 7",
"Abw. Kontoinhaber 7",
"Kennz. Haupt-Bankverb. 7",
"Bankverb 7 gültig von",
"Bankverb 7 gültig bis",
"Bankleitzahl 8",
"Bankbezeichnung 8",
"Bankkonto-Nummer 8",
"Länderkennzeichen 8",
"IBAN 8",
"Leerfeld 9",
"SWIFT-Code 8",
"Abw. Kontoinhaber 8",
"Kennz. Haupt-Bankverb. 8",
"Bankverb 8 gültig von",
"Bankverb 8 gültig bis",
"Bankleitzahl 9",
"Bankbezeichnung 9",
"Bankkonto-Nummer 9",
"Länderkennzeichen 9",
"IBAN 9",
"Leerfeld 10",
"SWIFT-Code 9",
"Abw. Kontoinhaber 9",
"Kennz. Haupt-Bankverb. 9",
"Bankverb 9 gültig von",
"Bankverb 9 gültig bis",
"Bankleitzahl 10",
"Bankbezeichnung 10",
"Bankkonto-Nummer 10",
"Länderkennzeichen 10",
"IBAN 10",
"Leerfeld 11",
"SWIFT-Code 10",
"Abw. Kontoinhaber 10",
"Kennz. Haupt-Bankverb. 10",
"Bankverb 10 gültig von",
"Bankverb 10 gültig bis",
"Nummer Fremdsystem",
"Insolvent",
"SEPA-Mandatsreferenz 1",
"SEPA-Mandatsreferenz 2",
"SEPA-Mandatsreferenz 3",
"SEPA-Mandatsreferenz 4",
"SEPA-Mandatsreferenz 5",
"SEPA-Mandatsreferenz 6",
"SEPA-Mandatsreferenz 7",
"SEPA-Mandatsreferenz 8",
"SEPA-Mandatsreferenz 9",
"SEPA-Mandatsreferenz 10",
"Verknüpftes OPOS-Konto",
"Mahnsperre bis",
"Lastschriftsperre bis",
"Zahlungssperre bis",
"Gebührenberechnung",
"Mahngebühr 1",
"Mahngebühr 2",
"Mahngebühr 3",
"Pauschalberechnung",
"Verzugspauschale 1",
"Verzugspauschale 2",
"Verzugspauschale 3",
"Alternativer Suchname",
"Status",
"Anschrift manuell geändert (Korrespondenzadresse)",
"Anschrift individuell (Korrespondenzadresse)",
"Anschrift manuell geändert (Rechnungsadresse)",
"Anschrift individuell (Rechnungsadresse)",
"Fristberechnung bei Debitor",
"Mahnfrist 1",
"Mahnfrist 2",
"Mahnfrist 3",
"Letzte Frist"
]
ACCOUNT_NAME_COLUMNS = [
# Account number
"Konto",
# Account name
"Kontenbeschriftung",
# Language of the account name
# "de-DE" or "en-GB"
"Sprach-ID"
]
QUERY_REPORT_COLUMNS = [
{
"label": "Umsatz (ohne Soll/Haben-Kz)",
"fieldname": "Umsatz (ohne Soll/Haben-Kz)",
"fieldtype": "Currency",
},
{
"label": "Soll/Haben-Kennzeichen",
"fieldname": "Soll/Haben-Kennzeichen",
"fieldtype": "Data",
},
{
"label": "Kontonummer",
"fieldname": "Kontonummer",
"fieldtype": "Data",
},
{
"label": "Gegenkonto (ohne BU-Schlüssel)",
"fieldname": "Gegenkonto (ohne BU-Schlüssel)",
"fieldtype": "Data",
},
{
"label": "Belegdatum",
"fieldname": "Belegdatum",
"fieldtype": "Date",
},
{
"label": "Buchungstext",
"fieldname": "Buchungstext",
"fieldtype": "Text",
},
{
"label": "Beleginfo - Art 1",
"fieldname": "Beleginfo - Art 1",
"fieldtype": "Data",
},
{
"label": "Beleginfo - Inhalt 1",
"fieldname": "Beleginfo - Inhalt 1",
"fieldtype": "Data",
},
{
"label": "Beleginfo - Art 2",
"fieldname": "Beleginfo - Art 2",
"fieldtype": "Data",
},
{
"label": "Beleginfo - Inhalt 2",
"fieldname": "Beleginfo - Inhalt 2",
"fieldtype": "Data",
}
]
class DataCategory():
"""Field of the CSV Header."""
DEBTORS_CREDITORS = "16"
ACCOUNT_NAMES = "20"
TRANSACTIONS = "21"
POSTING_TEXT_CONSTANTS = "67"
class FormatName():
"""Field of the CSV Header, corresponds to DataCategory."""
DEBTORS_CREDITORS = "Debitoren/Kreditoren"
ACCOUNT_NAMES = "Kontenbeschriftungen"
TRANSACTIONS = "Buchungsstapel"
POSTING_TEXT_CONSTANTS = "Buchungstextkonstanten"
class Transactions():
DATA_CATEGORY = DataCategory.TRANSACTIONS
FORMAT_NAME = FormatName.TRANSACTIONS
COLUMNS = TRANSACTION_COLUMNS
class DebtorsCreditors():
DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS
FORMAT_NAME = FormatName.DEBTORS_CREDITORS
COLUMNS = DEBTOR_CREDITOR_COLUMNS
class AccountNames():
DATA_CATEGORY = DataCategory.ACCOUNT_NAMES
FORMAT_NAME = FormatName.ACCOUNT_NAMES
COLUMNS = ACCOUNT_NAME_COLUMNS

View File

@ -0,0 +1,244 @@
# coding=utf-8
from __future__ import unicode_literals
import os
import json
import zipfile
from six import BytesIO
from unittest import TestCase
import frappe
from frappe.utils import getdate, today, now_datetime, cstr
from frappe.test_runner import make_test_objects
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
from erpnext.regional.report.datev.datev import validate
from erpnext.regional.report.datev.datev import get_transactions
from erpnext.regional.report.datev.datev import get_customers
from erpnext.regional.report.datev.datev import get_suppliers
from erpnext.regional.report.datev.datev import get_account_names
from erpnext.regional.report.datev.datev import get_datev_csv
from erpnext.regional.report.datev.datev import get_header
from erpnext.regional.report.datev.datev import download_datev_csv
from erpnext.regional.report.datev.datev_constants import DataCategory
from erpnext.regional.report.datev.datev_constants import Transactions
from erpnext.regional.report.datev.datev_constants import DebtorsCreditors
from erpnext.regional.report.datev.datev_constants import AccountNames
from erpnext.regional.report.datev.datev_constants import QUERY_REPORT_COLUMNS
def make_company(company_name, abbr):
if not frappe.db.exists("Company", company_name):
company = frappe.get_doc({
"doctype": "Company",
"company_name": company_name,
"abbr": abbr,
"default_currency": "EUR",
"country": "Germany",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "SKR04 mit Kontonummern"
})
company.insert()
else:
company = frappe.get_doc("Company", company_name)
# indempotent
company.create_default_warehouses()
if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}):
company.create_default_cost_center()
company.save()
return company
def setup_fiscal_year():
fiscal_year = None
year = cstr(now_datetime().year)
if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"):
try:
fiscal_year = frappe.get_doc({
"doctype": "Fiscal Year",
"year": year,
"year_start_date": "{0}-01-01".format(year),
"year_end_date": "{0}-12-31".format(year)
})
fiscal_year.insert()
except frappe.NameError:
pass
if fiscal_year:
fiscal_year.set_as_default()
def make_customer_with_account(customer_name, company):
acc_name = frappe.db.get_value("Account", {
"account_name": customer_name,
"company": company.name
}, "name")
if not acc_name:
acc = frappe.get_doc({
"doctype": "Account",
"parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG",
"account_name": customer_name,
"company": company.name,
"account_type": "Receivable",
"account_number": "10001"
})
acc.insert()
acc_name = acc.name
if not frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc({
"doctype": "Customer",
"customer_name": customer_name,
"customer_type": "Company",
"accounts": [{
"company": company.name,
"account": acc_name
}]
})
customer.insert()
else:
customer = frappe.get_doc("Customer", customer_name)
return customer
def make_item(item_code, company):
warehouse_name = frappe.db.get_value("Warehouse", {
"warehouse_name": "Stores",
"company": company.name
}, "name")
if not frappe.db.exists("Item", item_code):
item = frappe.get_doc({
"doctype": "Item",
"item_code": item_code,
"item_name": item_code,
"description": item_code,
"item_group": "All Item Groups",
"is_stock_item": 0,
"is_purchase_item": 0,
"is_customer_provided_item": 0,
"item_defaults": [{
"default_warehouse": warehouse_name,
"company": company.name
}]
})
item.insert()
else:
item = frappe.get_doc("Item", item_code)
return item
def make_datev_settings(company):
if not frappe.db.exists("DATEV Settings", company.name):
frappe.get_doc({
"doctype": "DATEV Settings",
"client": company.name,
"client_number": "12345",
"consultant_number": "67890"
}).insert()
class TestDatev(TestCase):
def setUp(self):
self.company = make_company("_Test GmbH", "_TG")
self.customer = make_customer_with_account("_Test Kunde GmbH", self.company)
self.filters = {
"company": self.company.name,
"from_date": today(),
"to_date": today()
}
make_datev_settings(self.company)
item = make_item("_Test Item", self.company)
setup_fiscal_year()
warehouse = frappe.db.get_value("Item Default", {
"parent": item.name,
"company": self.company.name
}, "default_warehouse")
income_account = frappe.db.get_value("Account", {
"account_number": "4200",
"company": self.company.name
}, "name")
tax_account = frappe.db.get_value("Account", {
"account_number": "3806",
"company": self.company.name
}, "name")
si = create_sales_invoice(
company=self.company.name,
customer=self.customer.name,
currency=self.company.default_currency,
debit_to=self.customer.accounts[0].account,
income_account="4200 - Erlöse - _TG",
expense_account="6990 - Herstellungskosten - _TG",
cost_center=self.company.cost_center,
warehouse=warehouse,
item=item.name,
do_not_save=1
)
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": tax_account,
"description": "Umsatzsteuer 19 %",
"rate": 19
})
si.save()
si.submit()
def test_columns(self):
def is_subset(get_data, allowed_keys):
"""
Validate that the dict contains only allowed keys.
Params:
get_data -- Function that returns a list of dicts.
allowed_keys -- List of allowed keys
"""
data = get_data(self.filters)
if data == []:
# No data and, therefore, no columns is okay
return True
actual_set = set(data[0].keys())
# allowed set must be interpreted as unicode to match the actual set
allowed_set = set({frappe.as_unicode(key) for key in allowed_keys})
return actual_set.issubset(allowed_set)
self.assertTrue(is_subset(get_transactions, Transactions.COLUMNS))
self.assertTrue(is_subset(get_customers, DebtorsCreditors.COLUMNS))
self.assertTrue(is_subset(get_suppliers, DebtorsCreditors.COLUMNS))
self.assertTrue(is_subset(get_account_names, AccountNames.COLUMNS))
def test_header(self):
self.assertTrue(Transactions.DATA_CATEGORY in get_header(self.filters, Transactions))
self.assertTrue(AccountNames.DATA_CATEGORY in get_header(self.filters, AccountNames))
self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors))
def test_csv(self):
test_data = [{
"Umsatz (ohne Soll/Haben-Kz)": 100,
"Soll/Haben-Kennzeichen": "H",
"Kontonummer": "4200",
"Gegenkonto (ohne BU-Schlüssel)": "10000",
"Belegdatum": today(),
"Buchungstext": "No remark",
"Beleginfo - Art 1": "Sales Invoice",
"Beleginfo - Inhalt 1": "SINV-0001"
}]
get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions)
def test_download(self):
"""Assert that the returned file is a ZIP file."""
download_datev_csv(self.filters)
# zipfile.is_zipfile() expects a file-like object
zip_buffer = BytesIO()
zip_buffer.write(frappe.response['filecontent'])
self.assertTrue(zipfile.is_zipfile(zip_buffer))

View File

@ -52,7 +52,7 @@ frappe.query_reports["GSTR-1"] = {
], ],
onload: function (report) { onload: function (report) {
report.page.add_inner_button(__("Download as Json"), function () { report.page.add_inner_button(__("Download as JSON"), function () {
var filters = report.get_values(); var filters = report.get_values();
const args = { const args = {

View File

@ -204,6 +204,40 @@ class Customer(TransactionBase):
else: else:
frappe.msgprint(_("Multiple Loyalty Program found for the Customer. Please select manually.")) frappe.msgprint(_("Multiple Loyalty Program found for the Customer. Please select manually."))
def create_onboarding_docs(self, args):
defaults = frappe.defaults.get_defaults()
for i in range(1, args.get('max_count')):
customer = args.get('customer_name_' + str(i))
if customer:
try:
doc = frappe.get_doc({
'doctype': self.doctype,
'customer_name': customer,
'customer_type': 'Company',
'customer_group': _('Commercial'),
'territory': defaults.get('country'),
'company': defaults.get('company')
}).insert()
if args.get('customer_email_' + str(i)):
create_contact(customer, self.doctype,
doc.name, args.get("customer_email_" + str(i)))
except frappe.NameError:
pass
def create_contact(contact, party_type, party, email):
"""Create contact based on given contact name"""
contact = contact.split(' ')
contact = frappe.get_doc({
'doctype': 'Contact',
'first_name': contact[0],
'last_name': len(contact) > 1 and contact[1] or ""
})
contact.append('email_ids', dict(email_id=email, is_primary=1))
contact.append('links', dict(link_doctype=party_type, link_name=party))
contact.insert()
@frappe.whitelist() @frappe.whitelist()
def get_loyalty_programs(doc): def get_loyalty_programs(doc):
''' returns applicable loyalty programs for a customer ''' ''' returns applicable loyalty programs for a customer '''

View File

@ -202,7 +202,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
} }
} }
// payment request // payment request
if(flt(doc.per_billed)==0) { if(flt(doc.per_billed)<100) {
this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create')); this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create'));
this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create')); this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create'));
} }

View File

@ -834,6 +834,10 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
for item in sales_order.items: for item in sales_order.items:
if item.supplier and item.supplier not in suppliers: if item.supplier and item.supplier not in suppliers:
suppliers.append(item.supplier) suppliers.append(item.supplier)
if not suppliers:
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
for supplier in suppliers: for supplier in suppliers:
po =frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) po =frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
if len(po) == 0: if len(po) == 0:

View File

@ -12,6 +12,7 @@ from erpnext.selling.doctype.sales_order.sales_order import make_work_orders
from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.accounts_controller import update_child_qty_rate
import json import json
from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
class TestSalesOrder(unittest.TestCase): class TestSalesOrder(unittest.TestCase):
def tearDown(self): def tearDown(self):
@ -819,6 +820,25 @@ class TestSalesOrder(unittest.TestCase):
mr_doc = frappe.get_doc('Material Request',mr.get('name')) mr_doc = frappe.get_doc('Material Request',mr.get('name'))
self.assertEqual(mr_doc.items[0].sales_order, so.name) self.assertEqual(mr_doc.items[0].sales_order, so.name)
def test_so_optional_blanket_order(self):
"""
Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1.
Second Sales Order should not add on to Blanket Orders Ordered Quantity.
"""
bo = make_blanket_order(blanket_order_type = "Selling", quantity = 10, rate = 10)
so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1)
so_doc = frappe.get_doc('Sales Order', so.get('name'))
# To test if the SO has a Blanket Order
self.assertTrue(so_doc.items[0].blanket_order)
so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0)
so_doc = frappe.get_doc('Sales Order', so.get('name'))
# To test if the SO does NOT have a Blanket Order
self.assertEqual(so_doc.items[0].blanket_order, None)
def make_sales_order(**args): def make_sales_order(**args):
so = frappe.new_doc("Sales Order") so = frappe.new_doc("Sales Order")
args = frappe._dict(args) args = frappe._dict(args)
@ -845,7 +865,8 @@ def make_sales_order(**args):
"warehouse": args.warehouse, "warehouse": args.warehouse,
"qty": args.qty or 10, "qty": args.qty or 10,
"uom": args.uom or None, "uom": args.uom or None,
"rate": args.rate or 100 "rate": args.rate or 100,
"against_blanket_order": args.against_blanket_order
}) })
so.delivery_date = add_days(so.transaction_date, 10) so.delivery_date = add_days(so.transaction_date, 10)

View File

@ -68,6 +68,7 @@
"target_warehouse", "target_warehouse",
"prevdoc_docname", "prevdoc_docname",
"col_break4", "col_break4",
"against_blanket_order",
"blanket_order", "blanket_order",
"blanket_order_rate", "blanket_order_rate",
"planning_section", "planning_section",
@ -574,6 +575,7 @@
"report_hide": 1 "report_hide": 1
}, },
{ {
"depends_on": "eval:doc.against_blanket_order",
"fieldname": "blanket_order", "fieldname": "blanket_order",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Blanket Order", "label": "Blanket Order",
@ -581,6 +583,7 @@
"options": "Blanket Order" "options": "Blanket Order"
}, },
{ {
"depends_on": "eval:doc.against_blanket_order",
"fieldname": "blanket_order_rate", "fieldname": "blanket_order_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Blanket Order Rate", "label": "Blanket Order Rate",
@ -741,11 +744,17 @@
"fieldname": "image_section", "fieldname": "image_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Image" "label": "Image"
},
{
"default": "0",
"fieldname": "against_blanket_order",
"fieldtype": "Check",
"label": "Against Blanket Order"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-10-10 08:46:26.244823", "modified": "2019-11-19 14:19:29.491945",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@ -1,611 +1,159 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-06-25 10:25:16", "creation": "2013-06-25 10:25:16",
"custom": 0,
"description": "Settings for Selling Module", "description": "Settings for Selling Module",
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"cust_master_name",
"campaign_naming_by",
"customer_group",
"territory",
"selling_price_list",
"close_opportunity_after_days",
"default_valid_till",
"column_break_5",
"so_required",
"dn_required",
"sales_update_frequency",
"maintain_same_sales_rate",
"editable_price_list_rate",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
"validate_selling_price",
"hide_tax_id"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Customer Name", "default": "Customer Name",
"fieldname": "cust_master_name", "fieldname": "cust_master_name",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Customer Naming By", "label": "Customer Naming By",
"length": 0, "options": "Customer Name\nNaming Series"
"no_copy": 0,
"options": "Customer Name\nNaming Series",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "campaign_naming_by", "fieldname": "campaign_naming_by",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Campaign Naming By", "label": "Campaign Naming By",
"length": 0, "options": "Campaign Name\nNaming Series"
"no_copy": 0,
"options": "Campaign Name\nNaming Series",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "customer_group", "fieldname": "customer_group",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Default Customer Group", "label": "Default Customer Group",
"length": 0, "options": "Customer Group"
"no_copy": 0,
"options": "Customer Group",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "territory", "fieldname": "territory",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Default Territory", "label": "Default Territory",
"length": 0, "options": "Territory"
"no_copy": 0,
"options": "Territory",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "selling_price_list", "fieldname": "selling_price_list",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Default Price List", "label": "Default Price List",
"length": 0, "options": "Price List"
"no_copy": 0,
"options": "Price List",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "15", "default": "15",
"description": "Auto close Opportunity after 15 days", "description": "Auto close Opportunity after 15 days",
"fieldname": "close_opportunity_after_days", "fieldname": "close_opportunity_after_days",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "label": "Close Opportunity After Days"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Close Opportunity After Days",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_valid_till", "fieldname": "default_valid_till",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "label": "Default Quotation Validity Days"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Quotation Validity Days",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "description": "Only for Stock Items",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "so_required", "fieldname": "so_required",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sales Order Required", "label": "Sales Order Required",
"length": 0, "options": "No\nYes"
"no_copy": 0,
"options": "No\nYes",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "dn_required", "fieldname": "dn_required",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Delivery Note Required", "label": "Delivery Note Required",
"length": 0, "options": "No\nYes"
"no_copy": 0,
"options": "No\nYes",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Each Transaction", "default": "Each Transaction",
"description": "How often should project and company be updated based on Sales Transactions.", "description": "How often should project and company be updated based on Sales Transactions.",
"fieldname": "sales_update_frequency", "fieldname": "sales_update_frequency",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sales Update Frequency", "label": "Sales Update Frequency",
"length": 0,
"no_copy": 0,
"options": "Each Transaction\nDaily\nMonthly", "options": "Each Transaction\nDaily\nMonthly",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maintain_same_sales_rate", "fieldname": "maintain_same_sales_rate",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Maintain Same Rate Throughout Sales Cycle"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Maintain Same Rate Throughout Sales Cycle",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "editable_price_list_rate", "fieldname": "editable_price_list_rate",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Allow user to edit Price List Rate in transactions"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow user to edit Price List Rate in transactions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "allow_multiple_items", "fieldname": "allow_multiple_items",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Allow Item to be added multiple times in a transaction"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Item to be added multiple times in a transaction",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "allow_against_multiple_purchase_orders", "fieldname": "allow_against_multiple_purchase_orders",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Allow multiple Sales Orders against a Customer's Purchase Order"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow multiple Sales Orders against a Customer's Purchase Order",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "validate_selling_price", "fieldname": "validate_selling_price",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Validate Selling Price for Item against Purchase Rate or Valuation Rate"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Validate Selling Price for Item against Purchase Rate or Valuation Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "hide_tax_id", "fieldname": "hide_tax_id",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Hide Customer's Tax Id from Sales Transactions"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Hide Customer's Tax Id from Sales Transactions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "modified": "2019-11-25 18:35:51.472653",
"max_attachments": 0,
"modified": "2018-06-25 12:56:16.332039",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 0,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 0,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestSellingSettings(unittest.TestCase):
pass

View File

@ -18,17 +18,18 @@ def execute(filters=None):
for wh, item_qty in warehouse.items(): for wh, item_qty in warehouse.items():
total += 1 total += 1
row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, if item_map.get(sbom):
item_map.get(sbom).stock_uom, wh] row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description,
available_qty = item_qty item_map.get(sbom).stock_uom, wh]
total_qty += flt(available_qty) available_qty = item_qty
row += [available_qty] total_qty += flt(available_qty)
row += [available_qty]
if available_qty: if available_qty:
data.append(row)
if (total == len(warehouse)):
row = ["", "", "Total", "", "", total_qty]
data.append(row) data.append(row)
if (total == len(warehouse)):
row = ["", "", "Total", "", "", total_qty]
data.append(row)
return columns, data return columns, data
def get_columns(): def get_columns():

View File

@ -0,0 +1,49 @@
{
"add_more_button": 1,
"app": "ERPNext",
"creation": "2019-11-15 14:44:10.065014",
"docstatus": 0,
"doctype": "Setup Wizard Slide",
"domains": [],
"help_links": [
{
"label": "Customers",
"video_id": "zsrrVDk6VBs"
}
],
"idx": 0,
"image_src": "/assets/erpnext/images/illustrations/customer.png",
"max_count": 3,
"modified": "2019-11-26 18:26:15.888794",
"modified_by": "Administrator",
"name": "Add A Few Customers",
"owner": "Administrator",
"ref_doctype": "Customer",
"slide_desc": "",
"slide_fields": [
{
"align": "",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"placeholder": "",
"reqd": 1
},
{
"align": "",
"fieldtype": "Column Break",
"reqd": 0
},
{
"align": "",
"fieldname": "customer_email",
"fieldtype": "Data",
"label": "Email ID",
"reqd": 1
}
],
"slide_order": 40,
"slide_title": "Add A Few Customers",
"slide_type": "Create",
"submit_method": ""
}

View File

@ -1,8 +0,0 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Setup Progress', {
refresh: function() {
}
});

View File

@ -1,123 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-08-27 21:01:42.032109",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "actions_sb",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Actions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "actions",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Actions",
"length": 0,
"no_copy": 0,
"options": "Setup Progress Action",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-09-21 11:52:56.106659",
"modified_by": "Administrator",
"module": "Setup",
"name": "Setup Progress",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@ -1,63 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
class SetupProgress(Document):
pass
def get_setup_progress():
if not getattr(frappe.local, "setup_progress", None):
frappe.local.setup_progress = frappe.get_doc("Setup Progress", "Setup Progress")
return frappe.local.setup_progress
def get_action_completed_state(action_name):
for d in get_setup_progress().actions:
if d.action_name == action_name:
return d.is_completed
def update_action_completed_state(action_name):
action_table_doc = [d for d in get_setup_progress().actions
if d.action_name == action_name][0]
update_action(action_table_doc)
def update_action(doc):
doctype = doc.action_doctype
docname = doc.action_document
field = doc.action_field
if not doc.is_completed:
if doc.min_doc_count:
if frappe.db.count(doctype) >= doc.min_doc_count:
doc.is_completed = 1
doc.save()
if docname and field:
d = frappe.get_doc(doctype, docname)
if d.get(field):
doc.is_completed = 1
doc.save()
def update_domain_actions(domain):
for d in get_setup_progress().actions:
domains = json.loads(d.domains)
if domains == [] or domain in domains:
update_action(d)
def get_domain_actions_state(domain):
state = {}
for d in get_setup_progress().actions:
domains = json.loads(d.domains)
if domains == [] or domain in domains:
state[d.action_name] = d.is_completed
return state
@frappe.whitelist()
def set_action_completed_state(action_name):
action_table_doc = [d for d in get_setup_progress().actions
if d.action_name == action_name][0]
action_table_doc.is_completed = 1
action_table_doc.save()

View File

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Setup Progress", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Setup Progress
() => frappe.tests.make('Setup Progress', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestSetupProgress(unittest.TestCase):
pass

View File

@ -1,253 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-08-27 21:00:40.715360",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "action_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Action Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "action_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Action Doctype",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "action_document",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Action Document",
"length": 0,
"no_copy": 0,
"options": "action_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "action_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Action Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "min_doc_count",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Min Doc Count",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "domains",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Domains",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_completed",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Is Completed",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-09-01 14:34:59.685730",
"modified_by": "Administrator",
"module": "Setup",
"name": "Setup Progress Action",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.model.document import Document
class SetupProgressAction(Document):
pass

View File

@ -1,71 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe, time
from frappe.utils.selenium_testdriver import TestDriver
def run_setup_wizard_test():
driver = TestDriver()
frappe.db.set_default('in_selenium', '1')
frappe.db.commit()
driver.login('#page-setup-wizard')
print('Running Setup Wizard Test...')
# Language slide
driver.wait_for_ajax(True)
time.sleep(1)
driver.set_select("language", "English (United States)")
driver.wait_for_ajax(True)
time.sleep(1)
driver.click(".next-btn")
# Region slide
driver.wait_for_ajax(True)
driver.set_select("country", "India")
driver.wait_for_ajax(True)
time.sleep(1)
driver.click(".next-btn")
# Profile slide
driver.set_field("full_name", "Great Tester")
driver.set_field("email", "great@example.com")
driver.set_field("password", "test")
driver.wait_for_ajax(True)
time.sleep(1)
driver.click(".next-btn")
time.sleep(1)
# domain slide
driver.set_multicheck("domains", ["Manufacturing"])
time.sleep(1)
driver.click(".next-btn")
# Org slide
driver.set_field("company_name", "For Testing")
time.sleep(1)
driver.print_console()
driver.click(".next-btn")
driver.set_field("company_tagline", "Just for GST")
driver.set_field("bank_account", "HDFC")
time.sleep(3)
driver.click(".complete-btn")
# Wait for desktop
driver.wait_for('#page-desktop', timeout=600)
driver.print_console()
time.sleep(3)
frappe.db.set_default('in_selenium', None)
frappe.db.set_value("Company", "For Testing", "write_off_account", "Write Off - FT")
frappe.db.set_value("Company", "For Testing", "exchange_gain_loss_account", "Exchange Gain/Loss - FT")
frappe.db.commit()
driver.close()
return True

View File

@ -0,0 +1,22 @@
{
"add_more_button": 0,
"app": "ERPNext",
"creation": "2019-11-26 17:01:26.671859",
"docstatus": 0,
"doctype": "Setup Wizard Slide",
"domains": [],
"help_links": [],
"idx": 0,
"image_src": "/assets/erpnext/images/illustrations/onboard.png",
"max_count": 0,
"modified": "2019-11-26 17:17:29.813299",
"modified_by": "Administrator",
"name": "Welcome to ERPNext!",
"owner": "Administrator",
"slide_desc": "Setting up an ERP can be overwhelming. But don't worry, we have got your back!<br>\nLet's setup your company.\nThis wizard will help you onboard to ERPNext in a short time!",
"slide_fields": [],
"slide_module": "Setup",
"slide_order": 10,
"slide_title": "Welcome to ERPNext!",
"slide_type": "Information"
}

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